{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tabs","type":"registry:ui","title":"Tabs","description":"Accessible tabs with a spring-animated pill or underline indicator, roving arrow-key focus, direction-aware panel transitions and lazily mounted panels that keep their state.","dependencies":["motion"],"registryDependencies":["utils"],"files":[{"path":"registry/ui/tabs.tsx","type":"registry:ui","content":"\"use client\";\n\nimport {\n  createContext,\n  useContext,\n  useId,\n  useRef,\n  useState,\n  type ButtonHTMLAttributes,\n  type HTMLAttributes,\n  type KeyboardEvent,\n} from \"react\";\nimport { MotionConfig, motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\n// Panels replay their entrance whenever they go from display:none to visible;\n// --ui-tab-dx makes them slide in from the side of the newly selected tab.\nconst css = `\n@keyframes ui-tab-in{from{opacity:0;translate:var(--ui-tab-dx,0) 0;filter:blur(2px)}}\n.ui-tab-panel:not([hidden]){animation:ui-tab-in .35s cubic-bezier(.2,.8,.2,1)}\n@media (prefers-reduced-motion:reduce){.ui-tab-panel:not([hidden]){animation:none}}\n`;\n\ntype Variant = \"pill\" | \"underline\";\ntype Ctx = {\n  value: string;\n  select: (value: string) => void;\n  /** -1 / 1: which way the selection moved, for the panel slide. */\n  dir: number;\n  /** Values ever selected; panels mount lazily on first visit, then stay mounted. */\n  visited: Set<string>;\n  orientation: \"horizontal\" | \"vertical\";\n  activation: \"automatic\" | \"manual\";\n  variant: Variant;\n  id: string;\n};\nconst TabsContext = createContext<Ctx | null>(null);\nconst ListContext = createContext<Variant>(\"pill\");\n\nfunction useTabs() {\n  const ctx = useContext(TabsContext);\n  if (!ctx) throw new Error(\"Tabs parts must be rendered inside <Tabs>.\");\n  return ctx;\n}\n\nconst safe = (v: string) => v.replace(/[^\\w-]/g, \"_\");\n\nexport interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  orientation?: \"horizontal\" | \"vertical\";\n  /** `automatic` selects on arrow-key focus; `manual` waits for Enter/Space. */\n  activationMode?: \"automatic\" | \"manual\";\n  /** Indicator style: a raised pill in a muted track, or a sliding underline. */\n  variant?: Variant;\n}\n\nexport function Tabs({\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  orientation = \"horizontal\",\n  activationMode = \"automatic\",\n  variant = \"pill\",\n  className,\n  children,\n  ...props\n}: TabsProps) {\n  const [inner, setInner] = useState(defaultValue);\n  const value = valueProp ?? inner;\n  const [dir, setDir] = useState(0);\n  const [visited, setVisited] = useState(() => new Set([value]));\n  const root = useRef<HTMLDivElement>(null);\n  const id = useId();\n  if (!visited.has(value)) setVisited(new Set(visited).add(value)); // controlled changes count as visits too\n\n  const select = (next: string) => {\n    if (next === value) return;\n    const tabs = [...(root.current?.querySelectorAll<HTMLElement>(`[data-tabs-root=\"${id}\"] [role=tab]`) ?? [])];\n    const at = (v: string) => tabs.findIndex((t) => t.dataset.value === v);\n    setDir(Math.sign(at(next) - at(value)));\n    if (valueProp === undefined) setInner(next);\n    onValueChange?.(next);\n  };\n\n  return (\n    <TabsContext value={{ value, select, dir, visited, orientation, activation: activationMode, variant, id }}>\n      <MotionConfig reducedMotion=\"user\">\n        <div\n          {...props}\n          ref={root}\n          data-tabs-root={id}\n          data-orientation={orientation}\n          className={cn(\"flex gap-4\", orientation === \"horizontal\" ? \"flex-col\" : \"flex-row\", className)}\n        >\n          <style href=\"ui-tabs\" precedence=\"default\">\n            {css}\n          </style>\n          {children}\n        </div>\n      </MotionConfig>\n    </TabsContext>\n  );\n}\n\nexport function TabsList({ className, onKeyDown, ...props }: HTMLAttributes<HTMLDivElement>) {\n  const { orientation, activation, variant } = useTabs();\n  const h = orientation === \"horizontal\";\n\n  // WAI-ARIA tabs: roving focus with arrows (wrapping), Home/End; disabled tabs are skipped.\n  const onKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(e);\n    const tabs = [...e.currentTarget.querySelectorAll<HTMLButtonElement>(\"[role=tab]:not(:disabled)\")];\n    const i = tabs.indexOf(document.activeElement as HTMLButtonElement);\n    if (i < 0) return;\n    const n = tabs.length;\n    const keys: Record<string, number> = h\n      ? { ArrowRight: i + 1, ArrowLeft: i - 1 + n, Home: 0, End: n - 1 }\n      : { ArrowDown: i + 1, ArrowUp: i - 1 + n, Home: 0, End: n - 1 };\n    const next = keys[e.key];\n    if (next === undefined) return;\n    e.preventDefault();\n    const t = tabs[next % n];\n    t.focus();\n    if (activation === \"automatic\") t.click();\n  };\n\n  return (\n    <ListContext value={variant}>\n      <div\n        role=\"tablist\"\n        aria-orientation={orientation}\n        {...props}\n        onKeyDown={onKey}\n        className={cn(\n          \"relative inline-flex w-fit shrink-0\",\n          h ? \"flex-row items-center\" : \"flex-col items-stretch\",\n          variant === \"pill\" && \"gap-1 rounded-xl bg-muted p-1\",\n          variant === \"underline\" && (h ? \"gap-2 border-b border-border\" : \"gap-1 border-l border-border\"),\n          className,\n        )}\n      />\n    </ListContext>\n  );\n}\n\nexport interface TabsTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  value: string;\n}\n\nexport function TabsTrigger({ value, className, children, onClick, disabled, ...props }: TabsTriggerProps) {\n  const ctx = useTabs();\n  const variant = useContext(ListContext);\n  const selected = ctx.value === value;\n  const h = ctx.orientation === \"horizontal\";\n  return (\n    <button\n      type=\"button\"\n      role=\"tab\"\n      id={`${ctx.id}-t-${safe(value)}`}\n      aria-selected={selected}\n      aria-controls={`${ctx.id}-p-${safe(value)}`}\n      tabIndex={selected ? 0 : -1}\n      data-value={value}\n      data-state={selected ? \"active\" : \"inactive\"}\n      disabled={disabled}\n      {...props}\n      onClick={(e) => {\n        onClick?.(e);\n        if (!e.defaultPrevented) ctx.select(value);\n      }}\n      className={cn(\n        \"relative inline-flex items-center justify-center gap-2 text-sm font-medium whitespace-nowrap outline-none select-none\",\n        \"transition-[color,scale] duration-200 focus-visible:ring-2 focus-visible:ring-ring/60 active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0\",\n        selected ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n        variant === \"pill\" && \"h-8 rounded-lg px-3\",\n        variant === \"underline\" && (h ? \"h-10 rounded-md px-3\" : \"h-9 justify-start rounded-md px-3\"),\n        className,\n      )}\n    >\n      {selected && (\n        <motion.span\n          aria-hidden\n          layoutId={`${ctx.id}-indicator`}\n          transition={{ type: \"spring\", stiffness: 420, damping: 34 }}\n          className={cn(\n            \"absolute\",\n            variant === \"pill\" && \"inset-0 rounded-lg bg-background shadow-sm ring-1 ring-border/60\",\n            variant === \"underline\" && (h ? \"inset-x-2 -bottom-px h-0.5 rounded-full bg-primary\" : \"inset-y-1.5 -left-px w-0.5 rounded-full bg-primary\"),\n          )}\n        />\n      )}\n      <span className=\"relative inline-flex items-center gap-2\">{children}</span>\n    </button>\n  );\n}\n\nexport interface TabsContentProps extends HTMLAttributes<HTMLDivElement> {\n  value: string;\n  /** Mount even before the tab is first opened (e.g. for SEO or form fields). */\n  forceMount?: boolean;\n}\n\nexport function TabsContent({ value, forceMount = false, className, style, ...props }: TabsContentProps) {\n  const ctx = useTabs();\n  const selected = ctx.value === value;\n  if (!forceMount && !ctx.visited.has(value)) return null;\n  return (\n    <div\n      role=\"tabpanel\"\n      id={`${ctx.id}-p-${safe(value)}`}\n      aria-labelledby={`${ctx.id}-t-${safe(value)}`}\n      tabIndex={0}\n      hidden={!selected}\n      {...props}\n      style={{ [\"--ui-tab-dx\" as string]: `${ctx.dir * 14}px`, ...style }}\n      className={cn(\"ui-tab-panel flex-1 rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring/60\", className)}\n    />\n  );\n}\n"}]}