{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "title": "Tabs",
  "description": "An accessible, animated tabs component built on Base UI with a spring-animated sliding active-tab indicator.",
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/base/ui/tabs.tsx",
      "content": "\"use client\"\n\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\"\nimport { cva } from \"class-variance-authority\"\nimport { motion } from \"motion/react\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst tabsListVariants = cva(\n  \"relative inline-flex items-center data-[orientation=vertical]:flex-col\",\n  {\n    variants: {\n      variant: {\n        default: \"w-fit gap-1 rounded-lg bg-muted p-1 text-muted-foreground\",\n        rounded: \"w-fit gap-1 rounded-full bg-muted p-1 text-muted-foreground\",\n        line: \"w-full gap-4 border-b border-border text-muted-foreground\",\n        ghost: \"w-fit gap-1 text-muted-foreground\",\n      },\n    },\n    defaultVariants: { variant: \"default\" },\n  }\n)\n\nconst tabsTriggerVariants = cva(\n  \"relative inline-flex h-7 items-center justify-center gap-1.5 px-3 text-sm font-medium whitespace-nowrap transition-colors outline-none select-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"rounded-md text-muted-foreground data-[active]:text-primary-foreground\",\n        rounded:\n          \"rounded-full text-muted-foreground data-[active]:text-primary-foreground\",\n        line: \"h-9 rounded-none px-1 pb-3 text-muted-foreground data-[active]:text-foreground\",\n        ghost: \"rounded-lg text-muted-foreground data-[active]:text-foreground\",\n      },\n    },\n    defaultVariants: { variant: \"default\" },\n  }\n)\n\nconst indicatorStylesByVariant = {\n  default: { className: \"bg-primary shadow-sm\", radius: 8 },\n  rounded: { className: \"bg-primary shadow-sm\", radius: 999 },\n  line: { className: \"bg-primary\", radius: 2 },\n  ghost: { className: \"bg-muted\", radius: 8 },\n} as const\n\n// Mirrors indicatorStylesByVariant's fill so the active trigger reads correctly\n// before the sliding indicator has measured (e.g. on mount), instead of only\n// getting its active text color with no background underneath.\nconst staticActiveStylesByVariant: Record<TabsVariant, string> = {\n  default: \"data-[active]:bg-primary data-[active]:shadow-sm\",\n  rounded: \"data-[active]:bg-primary data-[active]:shadow-sm\",\n  line: \"\",\n  ghost: \"data-[active]:bg-muted\",\n}\n\ntype TabsVariant = keyof typeof indicatorStylesByVariant\n\ninterface TabsContextValue {\n  activeValue: unknown\n  variant: TabsVariant\n}\n\nconst TabsContext = React.createContext<TabsContextValue | null>(null)\nconst TabsIndicatorMeasuredContext = React.createContext(false)\n\nfunction useTabsContext(component: string) {\n  const context = React.useContext(TabsContext)\n  if (!context) {\n    throw new Error(`\\`${component}\\` must be used within \\`Tabs\\`.`)\n  }\n  return context\n}\n\ninterface TabsProps extends TabsPrimitive.Root.Props {\n  variant?: TabsVariant\n}\n\nfunction Tabs({\n  className,\n  defaultValue,\n  value,\n  onValueChange,\n  variant = \"default\",\n  ...props\n}: TabsProps) {\n  const [uncontrolledValue, setUncontrolledValue] = React.useState(\n    defaultValue ?? null\n  )\n  const activeValue = value !== undefined ? value : uncontrolledValue\n\n  return (\n    <TabsContext.Provider value={{ activeValue, variant }}>\n      <TabsPrimitive.Root\n        data-slot=\"tabs\"\n        className={cn(\"flex flex-col gap-2\", className)}\n        defaultValue={defaultValue}\n        value={value}\n        onValueChange={(next, details) => {\n          setUncontrolledValue(next)\n          onValueChange?.(next, details)\n        }}\n        {...props}\n      />\n    </TabsContext.Provider>\n  )\n}\n\ninterface IndicatorRect {\n  x: number\n  y: number\n  width: number\n  height: number\n}\n\nfunction TabsList({ className, children, ...props }: TabsPrimitive.List.Props) {\n  const { activeValue, variant } = useTabsContext(\"TabsList\")\n  const listRef = React.useRef<HTMLDivElement>(null)\n  const [rect, setRect] = React.useState<IndicatorRect | null>(null)\n\n  React.useLayoutEffect(() => {\n    const list = listRef.current\n    if (!list) return\n\n    function measure() {\n      const activeTrigger = list?.querySelector<HTMLElement>(\n        '[data-slot=\"tabs-trigger\"][data-active]'\n      )\n      if (!activeTrigger) {\n        setRect(null)\n        return\n      }\n      if (variant === \"line\") {\n        setRect({\n          x: activeTrigger.offsetLeft,\n          y: activeTrigger.offsetTop + activeTrigger.offsetHeight - 2,\n          width: activeTrigger.offsetWidth,\n          height: 2,\n        })\n        return\n      }\n      setRect({\n        x: activeTrigger.offsetLeft,\n        y: activeTrigger.offsetTop,\n        width: activeTrigger.offsetWidth,\n        height: activeTrigger.offsetHeight,\n      })\n    }\n\n    measure()\n\n    const resizeObserver = new ResizeObserver(measure)\n    resizeObserver.observe(list)\n\n    const dirObserver = new MutationObserver(measure)\n    dirObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"dir\"],\n      subtree: true,\n    })\n\n    return () => {\n      resizeObserver.disconnect()\n      dirObserver.disconnect()\n    }\n  }, [activeValue, variant])\n\n  const indicatorStyle = indicatorStylesByVariant[variant]\n\n  return (\n    <TabsPrimitive.List\n      ref={listRef}\n      data-slot=\"tabs-list\"\n      className={cn(tabsListVariants({ variant }), className)}\n      {...props}\n    >\n      <TabsIndicatorMeasuredContext.Provider value={rect !== null}>\n        {rect && (\n          <motion.span\n            data-slot=\"tabs-indicator\"\n            className={cn(\"absolute top-0 left-0\", indicatorStyle.className)}\n            style={{ borderRadius: indicatorStyle.radius }}\n            initial={false}\n            animate={{\n              x: rect.x,\n              y: rect.y,\n              width: rect.width,\n              height: rect.height,\n            }}\n            transition={{ type: \"spring\", bounce: 0.15, duration: 0.3 }}\n          />\n        )}\n        {children}\n      </TabsIndicatorMeasuredContext.Provider>\n    </TabsPrimitive.List>\n  )\n}\n\nfunction TabsTrigger({ className, value, ...props }: TabsPrimitive.Tab.Props) {\n  const { variant } = useTabsContext(\"TabsTrigger\")\n  const measured = React.useContext(TabsIndicatorMeasuredContext)\n\n  return (\n    <TabsPrimitive.Tab\n      data-slot=\"tabs-trigger\"\n      value={value}\n      className={cn(\n        tabsTriggerVariants({ variant }),\n        !measured && staticActiveStylesByVariant[variant],\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction TabsContent({\n  className,\n  children,\n  ...props\n}: Omit<TabsPrimitive.Panel.Props, \"className\"> & { className?: string }) {\n  return (\n    <TabsPrimitive.Panel\n      data-slot=\"tabs-content\"\n      className=\"focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none\"\n      {...props}\n    >\n      <motion.div\n        className={className}\n        initial={false}\n        animate={{ opacity: 1, scale: 1 }}\n        transition={{ duration: 0.15, ease: \"easeOut\" }}\n      >\n        {children}\n      </motion.div>\n    </TabsPrimitive.Panel>\n  )\n}\n\nexport {\n  Tabs,\n  TabsList,\n  TabsTrigger,\n  TabsContent,\n  tabsListVariants,\n  tabsTriggerVariants,\n}\nexport type { TabsVariant }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}