{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tabs","type":"registry:ui","title":"Tabs","description":"Switch between related views of the same content without leaving the page.","dependencies":["@radix-ui/react-slot","@radix-ui/react-tabs","clsx","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/tabs.tsx","type":"registry:ui","target":"components/ui/tabs.tsx","content":"\"use client\";\n\nimport * as Primitive from \"@radix-ui/react-tabs\";\nimport { Slottable } from \"@radix-ui/react-slot\";\nimport {\n  useImperativeHandle,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n} from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./tabs.css\";\n\nexport const Tabs = Primitive.Root;\n\n/**\n * Tabs. The default underline variant moves one 2px accent line between tabs\n * on the settle curve while the panel swaps at once; labels never change\n * weight, so nothing shifts. The plate variant fills the active tab with the accent.\n */\nexport function TabsList({\n  className,\n  children,\n  ref,\n  variant = \"underline\",\n  ...props\n}: ComponentProps<typeof Primitive.List> & { variant?: \"underline\" | \"plate\" }) {\n  const list = useRef<HTMLDivElement>(null);\n  const [indicator, setIndicator] = useState<{\n    left: number;\n    top: number;\n    width: number;\n    height: number;\n  } | null>(null);\n  useImperativeHandle(ref, () => list.current!, []);\n  useLayoutEffect(() => {\n    const element = list.current;\n    if (!element) return;\n    const measure = () => {\n      const active = element.querySelector<HTMLElement>(\n        '[role=\"tab\"][data-state=\"active\"]',\n      );\n      setIndicator(\n        active\n          ? {\n              left:\n                active.getBoundingClientRect().left -\n                element.getBoundingClientRect().left +\n                element.scrollLeft,\n              top:\n                active.getBoundingClientRect().top -\n                element.getBoundingClientRect().top +\n                element.scrollTop,\n              width: active.offsetWidth,\n              height: active.offsetHeight,\n            }\n          : null,\n      );\n    };\n    measure();\n    const resize = new ResizeObserver(measure);\n    resize.observe(element);\n    element\n      .querySelectorAll('[role=\"tab\"]')\n      .forEach((tab) => resize.observe(tab));\n    const selection = new MutationObserver(measure);\n    selection.observe(element, {\n      subtree: true,\n      childList: true,\n      attributes: true,\n      attributeFilter: [\"data-state\", \"data-orientation\", \"dir\"],\n    });\n    return () => {\n      resize.disconnect();\n      selection.disconnect();\n    };\n  }, [children]);\n  return (\n    <Primitive.List\n      ref={list}\n      data-slot=\"tabs-list\"\n      data-variant={variant}\n      className={cn(\n        \"relative isolate inline-flex max-w-full gap-1 overflow-auto data-[orientation=vertical]:flex-col\",\n        variant === \"plate\" ? \"rounded-overlay bg-surface p-1\" : \"shadow-[inset_0_-1px_0_var(--border)]\",\n        className,\n      )}\n      {...props}\n    >\n      {indicator && (\n        <span\n          aria-hidden=\"true\"\n          data-slot=\"tabs-indicator\"\n          style={{\n            width: indicator.width,\n            height: indicator.height,\n            transform: `translate(${indicator.left}px, ${indicator.top}px)`,\n          }}\n        />\n      )}\n      <Slottable>{children}</Slottable>\n    </Primitive.List>\n  );\n}\n\nexport function TabsTrigger({\n  className,\n  ...props\n}: ComponentProps<typeof Primitive.Trigger>) {\n  return (\n    <Primitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"relative flex h-[var(--control-height)] shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-surface px-3 text-sm font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:outline-solid focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-45 data-[state=active]:text-foreground in-data-[variant=plate]:data-[state=active]:text-accent-foreground [&_svg]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** A count inside a tab, e.g. 128 invoices. Part of the tab's accessible name. */\nexport function TabsCount({ className, ...props }: ComponentProps<\"span\">) {\n  return <span data-slot=\"tabs-count\" className={className} {...props} />;\n}\n\nexport function TabsContent({\n  className,\n  ...props\n}: ComponentProps<typeof Primitive.Content>) {\n  return (\n    <Primitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\n        \"mt-5 min-w-0 rounded-surface outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-[state=inactive]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n"},{"path":"ui/tabs.css","type":"registry:file","target":"components/ui/tabs.css","content":"/* tabs.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"tabs-indicator\"] {\n    position: absolute;\n    top: 0;\n    left: 0;\n    z-index: -1;\n    pointer-events: none;\n    border-radius: var(--radius-surface);\n    background: var(--accent);\n    box-shadow: var(--shadow-control);\n    transition:\n      transform var(--duration-normal) var(--ease-settle),\n      width var(--duration-normal) var(--ease-settle),\n      height var(--duration-normal) var(--ease-settle);\n  }\n  /* Underline tabs: one 2px ink line travels; panels swap at once. */\n  [data-slot=\"tabs-list\"][data-variant=\"underline\"] > [data-slot=\"tabs-indicator\"] {\n    top: auto;\n    bottom: 0;\n    height: 2px !important;\n    border: 0;\n    border-radius: 2px;\n    background: var(--accent);\n    box-shadow: none;\n    transition:\n      transform 320ms var(--ease-settle),\n      width 320ms var(--ease-settle);\n  }\n  [data-slot=\"tabs-list\"][data-variant=\"underline\"] [data-slot=\"tabs-trigger\"] {\n    height: 40px;\n    border-radius: 8px 8px 0 0;\n  }\n  @media (hover: hover) {\n    [data-slot=\"tabs-list\"][data-variant=\"underline\"] [data-slot=\"tabs-trigger\"]:hover:not([data-state=\"active\"]) {\n      background: var(--muted);\n    }\n  }\n  [data-slot=\"tabs-count\"] {\n    padding: 0 5px;\n    border-radius: 4px;\n    font-variant-numeric: tabular-nums;\n    font-size: 12px;\n    line-height: 16px;\n    color: var(--muted-foreground);\n    background: var(--sunken);\n  }\n  [data-state=\"active\"] > [data-slot=\"tabs-count\"] {\n    color: var(--foreground);\n    background: var(--muted);\n  }\n\n  [data-slot=\"tabs-trigger\"] {\n    transition:\n      color var(--duration-fast) var(--ease-out),\n      background-color var(--duration-fast) var(--ease-out),\n      scale var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"tabs-trigger\"]:active:not(:disabled) {\n    scale: var(--motion-press-scale);\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"tabs-trigger\"],\n    [data-slot=\"tabs-indicator\"] {\n      transition: none;\n    }\n    [data-slot=\"tabs-list\"][data-variant=\"underline\"] > [data-slot=\"tabs-indicator\"] {\n      transition: none;\n    }\n    [data-slot=\"tabs-trigger\"]:active:not(:disabled) {\n      scale: none;\n    }\n    [data-slot=\"tabs-content\"] {\n      animation: none !important;\n    }\n  }\n}\n"}],"categories":["navigation"],"meta":{"tier":"free","license":"MIT","family":"animated-tabs","docs":"/docs/components/tabs"}}