{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "swipe-tabs",
  "title": "Swipe Tabs",
  "description": "Tabs whose panels sit in a scroll-snapping row so a swipe changes tab natively, with an indicator that follows the finger and full WAI-ARIA tab keyboard support.",
  "files": [
    {
      "path": "registry/tealmac/swipe-tabs.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface SwipeTab {\n  id: string\n  label: string\n  content: React.ReactNode\n}\n\nexport interface SwipeTabsProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"onChange\"\n> {\n  tabs: SwipeTab[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (id: string) => void\n  /** Accessible name of the tab list. */\n  label: string\n}\n\n/**\n * Tabs whose panels sit side by side in a scroll-snapping row, so a swipe\n * moves between them with the platform's own physics. The tab indicator\n * follows the scroll position; pressing a tab scrolls to its panel. Tabs follow\n * the WAI-ARIA tabs pattern, and panels that are off screen are `inert`.\n */\nexport function SwipeTabs({\n  tabs,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  label,\n  className,\n  ...props\n}: SwipeTabsProps) {\n  const baseId = React.useId()\n  const [state, setState] = React.useState(defaultValue ?? tabs[0]?.id)\n  const value = valueProp ?? state\n  const activeIndex = Math.max(\n    0,\n    tabs.findIndex((t) => t.id === value)\n  )\n  const pagerRef = React.useRef<HTMLDivElement>(null)\n  const tabRefs = React.useRef<(HTMLButtonElement | null)[]>([])\n  const [progress, setProgress] = React.useState(activeIndex)\n  const ignoreScroll = React.useRef(false)\n\n  const select = React.useCallback(\n    (id: string) => {\n      setState(id)\n      onValueChange?.(id)\n    },\n    [onValueChange]\n  )\n\n  const scrollTo = (index: number) => {\n    const panel = pagerRef.current?.children[index] as HTMLElement | undefined\n    if (!panel) return\n    const reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    ignoreScroll.current = true\n    panel.scrollIntoView({\n      behavior: reduce ? \"auto\" : \"smooth\",\n      inline: \"start\",\n      block: \"nearest\",\n    })\n    window.setTimeout(() => (ignoreScroll.current = false), reduce ? 0 : 450)\n  }\n\n  // Keep the pager in step when the value changes from outside.\n  const lastValue = React.useRef(value)\n  React.useEffect(() => {\n    if (lastValue.current !== value) {\n      lastValue.current = value\n      scrollTo(activeIndex)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [value])\n\n  const onScroll = () => {\n    const pager = pagerRef.current\n    if (!pager) return\n    const width = pager.clientWidth || 1\n    const raw = Math.abs(pager.scrollLeft) / width\n    setProgress(raw)\n    if (ignoreScroll.current) return\n    const nearest = Math.round(raw)\n    const tab = tabs[nearest]\n    if (tab && tab.id !== value) {\n      lastValue.current = tab.id\n      select(tab.id)\n    }\n  }\n\n  const onKeyDown = (event: React.KeyboardEvent) => {\n    const rtl = getComputedStyle(event.currentTarget).direction === \"rtl\"\n    const next = rtl ? \"ArrowLeft\" : \"ArrowRight\"\n    const prev = rtl ? \"ArrowRight\" : \"ArrowLeft\"\n    let to = -1\n    if (event.key === next) to = (activeIndex + 1) % tabs.length\n    else if (event.key === prev)\n      to = (activeIndex - 1 + tabs.length) % tabs.length\n    else if (event.key === \"Home\") to = 0\n    else if (event.key === \"End\") to = tabs.length - 1\n    if (to < 0) return\n    event.preventDefault()\n    select(tabs[to].id)\n    tabRefs.current[to]?.focus()\n  }\n\n  return (\n    <div\n      data-slot=\"swipe-tabs\"\n      className={cn(\"flex min-h-0 flex-col\", className)}\n      {...props}\n    >\n      {/* The tabs carry the tab stop (roving tabindex); the list handles arrows. */}\n      {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}\n      <div\n        role=\"tablist\"\n        aria-label={label}\n        onKeyDown={onKeyDown}\n        className=\"relative flex border-b\"\n      >\n        {tabs.map((tab, index) => {\n          const selected = index === activeIndex\n          return (\n            <button\n              key={tab.id}\n              ref={(el) => {\n                tabRefs.current[index] = el\n              }}\n              role=\"tab\"\n              id={`${baseId}-tab-${tab.id}`}\n              aria-selected={selected}\n              aria-controls={`${baseId}-panel-${tab.id}`}\n              tabIndex={selected ? 0 : -1}\n              type=\"button\"\n              onClick={() => select(tab.id)}\n              className={cn(\n                \"focus-visible:ring-ring min-h-11 flex-1 px-3 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-inset\",\n                selected ? \"text-foreground\" : \"text-muted-foreground\"\n              )}\n            >\n              {tab.label}\n            </button>\n          )\n        })}\n        <span\n          aria-hidden\n          className=\"bg-primary absolute bottom-0 h-0.5 rounded-full\"\n          style={{\n            width: `${100 / tabs.length}%`,\n            insetInlineStart: `${(progress * 100) / tabs.length}%`,\n          }}\n        />\n      </div>\n      <div\n        ref={pagerRef}\n        onScroll={onScroll}\n        className=\"flex min-h-0 flex-1 snap-x snap-mandatory [scrollbar-width:none] overflow-x-auto overscroll-x-contain [&::-webkit-scrollbar]:hidden\"\n      >\n        {tabs.map((tab, index) => (\n          <div\n            key={tab.id}\n            role=\"tabpanel\"\n            id={`${baseId}-panel-${tab.id}`}\n            aria-labelledby={`${baseId}-tab-${tab.id}`}\n            // Off-screen panels are not reachable by keyboard or screen reader.\n            inert={index !== activeIndex}\n            tabIndex={index === activeIndex ? 0 : undefined}\n            className=\"focus-visible:ring-ring w-full shrink-0 snap-start snap-always overflow-y-auto outline-none focus-visible:ring-2 focus-visible:ring-inset\"\n          >\n            {tab.content}\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}