{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-tabs",
  "title": "Animated Tabs",
  "description": "Tabs with a sliding indicator that animates to the active item.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/animated-tabs/animated-tabs.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/** A single tab descriptor. */\nexport type AnimatedTab = {\n  /** Visible label. */\n  label: string;\n  /** Stable identifier for the tab. */\n  value: string;\n};\n\n/** Props for {@link AnimatedTabs}. */\nexport type AnimatedTabsProps = React.ComponentPropsWithoutRef<\"div\"> & {\n  /** Initially active tab value. Defaults to the first tab. */\n  defaultValue?: string;\n  /** Called with the new value when the active tab changes. */\n  onValueChange?: (value: string) => void;\n  /** Tabs rendered in order. */\n  tabs: AnimatedTab[];\n};\n\ntype IndicatorStyle = {\n  left: number;\n  width: number;\n};\n\nfunction indicatorPosition(button: HTMLButtonElement | null): IndicatorStyle {\n  if (button === null) {\n    return { left: 0, width: 0 };\n  }\n\n  return { left: button.offsetLeft, width: button.offsetWidth };\n}\n\nfunction TabButton({\n  active,\n  label,\n  onSelect,\n  reference,\n}: {\n  active: boolean;\n  label: string;\n  onSelect: () => void;\n  reference: (node: HTMLButtonElement | null) => void;\n}) {\n  return (\n    <button\n      aria-selected={active}\n      className={cn(\n        \"relative z-10 rounded-md px-3 py-1.5 text-sm font-medium transition-colors\",\n        active ? \"text-primary-foreground\" : \"text-muted-foreground\",\n      )}\n      onClick={onSelect}\n      ref={reference}\n      role=\"tab\"\n      type=\"button\"\n    >\n      {label}\n    </button>\n  );\n}\n\n/**\n * Row of tabs with a pill that slides behind the active tab.\n *\n * Respects `prefers-reduced-motion`: the pill jumps without sliding.\n *\n * @example\n * ```tsx\n * <AnimatedTabs tabs={[{ value: \"a\", label: \"A\" }, { value: \"b\", label: \"B\" }]} />\n * ```\n */\nexport const AnimatedTabs = ({\n  className,\n  defaultValue,\n  onValueChange,\n  ref,\n  tabs,\n  ...props\n}: AnimatedTabsProps & { ref?: React.Ref<HTMLDivElement> }) => {\n  const [active, setActive] = React.useState(defaultValue ?? tabs[0]?.value);\n  const [indicator, setIndicator] = React.useState<IndicatorStyle>({\n    left: 0,\n    width: 0,\n  });\n  const buttons = React.useRef(new Map<string, HTMLButtonElement>());\n\n  React.useLayoutEffect(() => {\n    setIndicator(indicatorPosition(buttons.current.get(active ?? \"\") ?? null));\n  }, [active]);\n\n  const handleSelect = (value: string): void => {\n    setActive(value);\n    onValueChange?.(value);\n  };\n\n  return (\n    <div\n      className={cn(\n        \"relative inline-flex items-center gap-1 rounded-lg border border-border bg-muted p-1\",\n        className,\n      )}\n      ref={ref}\n      role=\"tablist\"\n      {...props}\n    >\n      <span\n        aria-hidden=\"true\"\n        className=\"absolute top-1 z-0 h-[calc(100%-0.5rem)] rounded-md bg-primary transition-all duration-300 ease-out motion-reduce:transition-none\"\n        style={{ left: indicator.left, width: indicator.width }}\n      />\n      {tabs.map((tab) => (\n        <TabButton\n          active={tab.value === active}\n          key={tab.value}\n          label={tab.label}\n          onSelect={() => {\n            handleSelect(tab.value);\n          }}\n          reference={(node) => {\n            if (node === null) {\n              buttons.current.delete(tab.value);\n            } else {\n              buttons.current.set(tab.value, node);\n            }\n          }}\n        />\n      ))}\n    </div>\n  );\n};\nAnimatedTabs.displayName = \"AnimatedTabs\";\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.2.1",
  "stability": "stable"
}
