{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "type": "registry:ui",
  "title": "Tabs",
  "description": "Switch between panels of related content with keyboard navigation and an animated active indicator.",
  "dependencies": [
    "@radix-ui/react-tabs",
    "@saasflare/ui",
    "class-variance-authority",
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/tabs.tsx",
      "type": "registry:ui",
      "content": "// @toreview\n\"use client\"\n\n/**\n * @fileoverview Saasflare Tabs — tabbed navigation with animated indicator.\n * @module packages/ui/components/ui/tabs\n * @layer core\n *\n * Self-contained implementation using Radix Tabs primitive directly.\n * The active-tab indicator is a single `m.div` inside `TabsList` that\n * animates `x`/`y`/`width`/`height` to track the active trigger via a\n * MutationObserver on `data-state`. No `LayoutGroup` — works under\n * `LazyMotion features={domAnimation}` strict mode.\n *\n * Layout indicator animation respects reduced-motion preference.\n *\n * @example\n * import { Tabs, TabsList, TabsTrigger, TabsContent } from \"@saasflare/ui\";\n * <Tabs defaultValue=\"overview\">\n *   <TabsList>\n *     <TabsTrigger value=\"overview\">Overview</TabsTrigger>\n *     <TabsTrigger value=\"settings\">Settings</TabsTrigger>\n *   </TabsList>\n *   <TabsContent value=\"overview\">...</TabsContent>\n *   <TabsContent value=\"settings\">...</TabsContent>\n * </Tabs>\n */\n\nimport * as React from \"react\"\nimport { m } from \"motion/react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\nimport { cn } from \"@saasflare/ui\"\nimport { useSaasflareProps, type SaasflareComponentProps } from \"@saasflare/ui\"\nimport { useSaasflareMotion, spring } from \"@saasflare/ui\"\n\n/**\n * Props for {@link Tabs}. Extends {@link SaasflareComponentProps} so\n * `surface`, `radius`, `animated`, and `iconWeight` can be supplied\n * per-instance or inherited from the provider.\n */\ninterface TabsProps\n  extends Omit<React.ComponentProps<typeof TabsPrimitive.Root>, keyof SaasflareComponentProps>,\n    SaasflareComponentProps {}\n\n/**\n * Tabbed navigation root built on Radix Tabs, with an animated indicator that\n * tracks the active trigger. Owns the active value and orientation; compose\n * with {@link TabsList}, {@link TabsTrigger}, and {@link TabsContent}.\n *\n * @component\n * @layer core\n */\nfunction Tabs({\n  className,\n  orientation = \"horizontal\",\n  surface,\n  radius,\n  animated,\n  iconWeight,\n  ...props\n}: TabsProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n\n  return (\n    <TabsPrimitive.Root\n      {...props}\n      data-slot=\"tabs\"\n      data-orientation={orientation}\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n      orientation={orientation}\n      className={cn(\n        \"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col\",\n        className\n      )}\n    />\n  )\n}\n\n/**\n * Variant classes for {@link TabsList} — `default` (filled muted track) vs\n * `line` (transparent, underline-style); consumed by `TabsList` and exported\n * for class reuse.\n */\nconst tabsListVariants = cva(\n  \"group/tabs-list relative inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-muted\",\n        line: \"gap-1 bg-transparent\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\ninterface IndicatorPos {\n  x: number\n  y: number\n  width: number\n  height: number\n}\n\n/**\n * Props for {@link TabsList}. `variant` selects the track style\n * (`\"default\"` filled vs `\"line\"` transparent).\n */\ninterface TabsListProps\n  extends Omit<\n      React.ComponentProps<typeof TabsPrimitive.List>,\n      keyof SaasflareComponentProps\n    >,\n    SaasflareComponentProps,\n    VariantProps<typeof tabsListVariants> {}\n\n/**\n * Container for tab triggers. Renders the single animated active-tab\n * indicator that follows the active trigger via measured position.\n *\n * @component\n * @layer core\n */\nfunction TabsList({\n  className,\n  variant = \"default\",\n  children,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n  ...props\n}: TabsListProps) {\n  const listRef = React.useRef<HTMLDivElement>(null)\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  const motion = useSaasflareMotion(sf.animated, spring)\n  const [pos, setPos] = React.useState<IndicatorPos | null>(null)\n\n  React.useLayoutEffect(() => {\n    const list = listRef.current\n    if (!list) return\n\n    const measure = () => {\n      const active = list.querySelector<HTMLElement>(\n        '[data-slot=\"tabs-trigger\"][data-state=\"active\"]'\n      )\n      if (!active) {\n        setPos(null)\n        return\n      }\n      const listRect = list.getBoundingClientRect()\n      const activeRect = active.getBoundingClientRect()\n      const next: IndicatorPos = {\n        x: activeRect.left - listRect.left,\n        y: activeRect.top - listRect.top,\n        width: activeRect.width,\n        height: activeRect.height,\n      }\n      setPos((prev) =>\n        prev &&\n        prev.x === next.x &&\n        prev.y === next.y &&\n        prev.width === next.width &&\n        prev.height === next.height\n          ? prev\n          : next\n      )\n    }\n\n    measure()\n\n    const mutationObserver = new MutationObserver(measure)\n    mutationObserver.observe(list, {\n      subtree: true,\n      attributes: true,\n      attributeFilter: [\"data-state\"],\n    })\n\n    const resizeObserver = new ResizeObserver(measure)\n    resizeObserver.observe(list)\n\n    return () => {\n      mutationObserver.disconnect()\n      resizeObserver.disconnect()\n    }\n  }, [])\n\n  return (\n    <TabsPrimitive.List\n      ref={listRef}\n      data-slot=\"tabs-list\"\n      data-variant={variant}\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n      className={cn(tabsListVariants({ variant }), className)}\n      {...props}\n    >\n      {pos !== null && (\n        <m.div\n          data-slot=\"tabs-indicator\"\n          aria-hidden\n          initial={false}\n          animate={{\n            x: pos.x,\n            y: pos.y,\n            width: pos.width,\n            height: pos.height,\n          }}\n          transition={motion.transition}\n          className=\"pointer-events-none absolute top-0 left-0 rounded-md bg-background shadow-sm dark:border dark:border-input dark:bg-input/30\"\n          style={{ zIndex: 0 }}\n        />\n      )}\n      {children}\n    </TabsPrimitive.List>\n  )\n}\n\n/**\n * Tab trigger. The active-tab indicator is rendered once at the\n * `TabsList` level and animates between triggers via measured position;\n * triggers themselves only own their content + state styling.\n *\n * @component\n * @layer core\n */\ninterface TabsTriggerProps\n  extends Omit<\n      React.ComponentProps<typeof TabsPrimitive.Trigger>,\n      keyof SaasflareComponentProps\n    >,\n    SaasflareComponentProps {}\n\n/**\n * Tab trigger button that activates its associated {@link TabsContent} panel.\n *\n * @component\n * @layer core\n */\nfunction TabsTrigger({\n  className,\n  children,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n  ...props\n}: TabsTriggerProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n      className={cn(\n        \"relative z-10 inline-flex h-[calc(100%-1px)] flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-colors group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        \"data-[state=active]:text-foreground dark:data-[state=active]:text-foreground\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </TabsPrimitive.Trigger>\n  )\n}\n\n/**\n * Props for {@link TabsContent}.\n */\ninterface TabsContentProps\n  extends Omit<\n      React.ComponentProps<typeof TabsPrimitive.Content>,\n      keyof SaasflareComponentProps\n    >,\n    SaasflareComponentProps {}\n\n/**\n * Panel shown when its `value` matches the active tab.\n *\n * @component\n * @layer core\n */\nfunction TabsContent({\n  className,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n  ...props\n}: TabsContentProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n      className={cn(\"flex-1 outline-none\", className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Tabs,\n  TabsList,\n  TabsTrigger,\n  TabsContent,\n  tabsListVariants,\n  type TabsProps,\n  type TabsListProps,\n  type TabsTriggerProps,\n  type TabsContentProps,\n}\n",
      "target": "components/ui/tabs.tsx"
    }
  ],
  "$meta": {
    "source": "https://ui.saasflare.io/r/tabs.json"
  }
}
