{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "title": "Tabs",
  "author": "Tori Bryan",
  "description": "Switches between views of one subject in place, as a segmented control or a row of tabs with an underline; scrolls sideways when it runs out of room.",
  "dependencies": [
    "@base-ui/react",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/tabs.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\"\nimport { cva } from \"class-variance-authority\"\n\nimport { mergeRefs } from \"@/lib/merge-refs\"\nimport { cn } from \"@/lib/utils\"\n\ntype TabsVariant = \"default\" | \"line\"\ntype TabsSize = \"sm\" | \"default\"\n\nconst TabsListContext = React.createContext<{\n  variant: TabsVariant\n  size: TabsSize\n}>({ variant: \"default\", size: \"default\" })\n\n/** Groups the list of tabs and the panels they show. */\nfunction Tabs({ className, ...props }: TabsPrimitive.Root.Props) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      className={cn(\n        \"flex gap-3 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:items-start data-[orientation=vertical]:gap-6\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nconst tabsListVariants = cva(\n  \"relative flex w-fit max-w-full shrink-0 items-center data-[orientation=horizontal]:[scrollbar-width:none] data-[orientation=horizontal]:overflow-x-auto data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"rounded-4xl bg-muted p-0.5 data-[orientation=vertical]:rounded-3xl\",\n        line: \"gap-1 data-[orientation=horizontal]:pb-1 data-[orientation=vertical]:ps-2\",\n      },\n      size: {\n        sm: \"\",\n        default: \"\",\n      },\n    },\n    compoundVariants: [\n      {\n        variant: \"default\",\n        size: \"sm\",\n        className: \"data-[orientation=horizontal]:h-8\",\n      },\n      {\n        variant: \"default\",\n        size: \"default\",\n        className: \"data-[orientation=horizontal]:h-9\",\n      },\n    ],\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nconst tabsTriggerVariants = cva(\n  \"relative z-10 inline-flex shrink-0 items-center justify-center gap-1.5 rounded-4xl px-3 text-sm font-medium whitespace-nowrap text-muted-foreground transition-colors outline-none select-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring-subtle focus-visible:ring-inset data-[orientation=vertical]:justify-start data-disabled:pointer-events-none data-disabled:opacity-50 data-active:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      variant: {\n        // The track is already muted, so hover only lifts the text; the\n        // raised pill under the active tab is the indicator.\n        default: \"\",\n        line: \"hover:bg-muted\",\n      },\n      size: {\n        sm: \"\",\n        default: \"\",\n      },\n    },\n    compoundVariants: [\n      { variant: \"default\", size: \"sm\", className: \"h-7 px-2.5\" },\n      { variant: \"default\", size: \"default\", className: \"h-8\" },\n      { variant: \"line\", size: \"sm\", className: \"h-8 px-2.5\" },\n      { variant: \"line\", size: \"default\", className: \"h-9\" },\n    ],\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nconst tabsIndicatorVariants = cva(\n  \"absolute z-0 transition-[left,top,width,height] duration-200 ease-out motion-reduce:transition-none\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"top-(--active-tab-top) left-(--active-tab-left) h-(--active-tab-height) w-(--active-tab-width) rounded-4xl bg-background shadow-sm ring-1 ring-border dark:bg-input\",\n        line: \"rounded-full bg-primary data-[orientation=horizontal]:bottom-0 data-[orientation=horizontal]:left-(--active-tab-left) data-[orientation=horizontal]:h-0.5 data-[orientation=horizontal]:w-(--active-tab-width) data-[orientation=vertical]:start-0 data-[orientation=vertical]:top-(--active-tab-top) data-[orientation=vertical]:h-(--active-tab-height) data-[orientation=vertical]:w-0.5\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\n/*\n * Keeps the active tab inside a list that scrolls sideways, on mount and\n * whenever the selection moves. Sets the list's own scroll rather than\n * calling scrollIntoView, which would also scroll the page.\n */\nfunction useActiveTabInView(list: React.RefObject<HTMLDivElement | null>) {\n  React.useEffect(() => {\n    const node = list.current\n    if (!node) return\n    const reveal = (behavior: ScrollBehavior) => {\n      if (node.scrollWidth <= node.clientWidth) return\n      const active = node.querySelector<HTMLElement>(\n        \"[data-slot=tabs-trigger][data-active]\"\n      )\n      if (!active) return\n      const start = active.offsetLeft\n      const end = start + active.offsetWidth\n      const inset = 16\n      let left: number | null = null\n      if (start < node.scrollLeft) left = start - inset\n      else if (end > node.scrollLeft + node.clientWidth)\n        left = end - node.clientWidth + inset\n      if (left === null) return\n      const reduce = window.matchMedia(\n        \"(prefers-reduced-motion: reduce)\"\n      ).matches\n      node.scrollTo({ left, behavior: reduce ? \"instant\" : behavior })\n    }\n    reveal(\"instant\")\n    const observer = new MutationObserver(() => reveal(\"smooth\"))\n    observer.observe(node, {\n      subtree: true,\n      attributes: true,\n      attributeFilter: [\"data-active\"],\n    })\n    return () => observer.disconnect()\n  }, [list])\n}\n\ntype TabsListProps = TabsPrimitive.List.Props & {\n  /** `default` is a segmented control: a muted track with the active tab raised on a pill. `line` is a bare row with a bar under the active tab, or beside it when vertical. */\n  variant?: TabsVariant\n  /** Height of the row: `sm` is 32px and `default` 36px, matching Button and Input. */\n  size?: TabsSize\n}\n\n/** The row of tabs. Sets the variant and size for every trigger in it, and scrolls sideways when it doesn't fit. */\nfunction TabsList({\n  variant = \"default\",\n  size = \"default\",\n  className,\n  children,\n  ref,\n  ...props\n}: TabsListProps) {\n  const listRef = React.useRef<HTMLDivElement>(null)\n  const mergedListRef = React.useMemo(() => mergeRefs(listRef, ref), [ref])\n  useActiveTabInView(listRef)\n  const context = React.useMemo(() => ({ variant, size }), [variant, size])\n\n  return (\n    <TabsListContext.Provider value={context}>\n      <TabsPrimitive.List\n        ref={mergedListRef}\n        data-slot=\"tabs-list\"\n        data-variant={variant}\n        data-size={size}\n        className={cn(tabsListVariants({ variant, size }), className)}\n        {...props}\n      >\n        {children}\n        <TabsPrimitive.Indicator\n          data-slot=\"tabs-indicator\"\n          className={tabsIndicatorVariants({ variant })}\n        />\n      </TabsPrimitive.List>\n    </TabsListContext.Provider>\n  )\n}\n\n/** One tab. Holds a label, and an icon or a Count beside it. */\nfunction TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {\n  const { variant, size } = React.useContext(TabsListContext)\n  return (\n    <TabsPrimitive.Tab\n      data-slot=\"tabs-trigger\"\n      className={cn(tabsTriggerVariants({ variant, size }), className)}\n      {...props}\n    />\n  )\n}\n\n/** The panel a tab shows. Only the active one is in the DOM unless `keepMounted` is set. */\nfunction TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {\n  return (\n    <TabsPrimitive.Panel\n      data-slot=\"tabs-content\"\n      className={cn(\n        \"min-w-0 flex-1 rounded-md text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring-subtle\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  tabsListVariants,\n  TabsTrigger,\n  type TabsListProps,\n  type TabsSize,\n  type TabsVariant,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/lib/merge-refs.ts",
      "content": "import * as React from \"react\"\n\n/**\n * One callback ref that sets every ref given, so a part can keep its own\n * ref on an element and still hand the element to a caller's `ref`. Undo\n * runs through React 19's ref cleanup, which calls a function ref with\n * null and empties an object ref.\n */\nexport function mergeRefs<T>(\n  ...refs: (React.Ref<T> | undefined)[]\n): React.RefCallback<T> {\n  return (node) => {\n    const cleanups = refs.map((ref) => {\n      if (typeof ref === \"function\") {\n        const cleanup = ref(node)\n        return typeof cleanup === \"function\" ? cleanup : () => ref(null)\n      }\n      if (ref) {\n        ref.current = node\n        return () => {\n          ref.current = null\n        }\n      }\n      return undefined\n    })\n    return () => {\n      for (const cleanup of cleanups) cleanup?.()\n    }\n  }\n}\n",
      "type": "registry:lib"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ring-subtle": "var(--ring-subtle)"
    },
    "light": {
      "ring-subtle": "color-mix(in oklch, var(--color-neutral-400) 50%, transparent)"
    },
    "dark": {
      "ring-subtle": "color-mix(in oklch, var(--color-neutral-500) 50%, transparent)"
    }
  },
  "meta": {
    "tier": "base-components",
    "group": "Navigation",
    "docs": "https://fibo.toribryan.com/?path=/docs/base-components-navigation-tabs--docs"
  },
  "docs": "Docs and live examples: https://fibo.toribryan.com/?path=/docs/base-components-navigation-tabs--docs",
  "categories": [
    "base-components",
    "navigation"
  ],
  "type": "registry:ui"
}