{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tabs","type":"registry:component","title":"Tabs","description":"Pill, segment, or underline tabs with an indicator that slides to the selected tab.","author":"Ryan","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/tabs.tsx","type":"registry:component","target":"@components/motion/tabs.tsx","content":"\"use client\";\n// easeui.dev/components/motion/tabs\n\nimport { MotionConfig, motion, useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  type KeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype Variant = \"pill\" | \"segment\" | \"underline\";\n\ninterface TabsContextValue {\n  value: string;\n  select: (value: string) => void;\n  baseId: string;\n  variant: Variant;\n}\n\nconst TabsContext = createContext<TabsContextValue | null>(null);\n\nfunction useTabs(part: string) {\n  const ctx = useContext(TabsContext);\n  if (!ctx) throw new Error(`${part} must be used inside <Tabs>`);\n  return ctx;\n}\n\nconst EASE = [0.23, 1, 0.32, 1] as const;\n// The indicator glides between tabs. A tween with no bounce, so a scrolling\n// tab list never overshoots and flashes a scrollbar.\nconst INDICATOR_TRANSITION = { duration: 0.2, ease: EASE } as const;\n\nconst LIST_CLASS: Record<Variant, string> = {\n  pill: \"inline-flex items-center gap-1 rounded-full bg-muted p-1\",\n  segment: \"inline-flex items-center gap-0.5 rounded-lg bg-muted p-0.5\",\n  underline: \"flex items-center gap-5 border-b border-border\",\n};\n\nconst TRIGGER_CLASS: Record<Variant, string> = {\n  pill: \"min-h-9 rounded-full px-3.5\",\n  segment: \"min-h-8 rounded-md px-3\",\n  underline: \"-mb-px min-h-11 px-0.5\",\n};\n\nconst INDICATOR_CLASS: Record<Variant, string> = {\n  pill: \"absolute inset-0 rounded-full bg-background shadow-[0_1px_2px_rgb(0_0_0/0.12)]\",\n  segment: \"absolute inset-0 rounded-md bg-background shadow-[0_1px_2px_rgb(0_0_0/0.12)]\",\n  underline: \"absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-foreground\",\n};\n\nexport interface TabsProps {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Visual style. Default \"pill\". */\n  variant?: Variant;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function Tabs({\n  value,\n  defaultValue,\n  onValueChange,\n  variant = \"pill\",\n  className,\n  children,\n}: TabsProps) {\n  const [inner, setInner] = useState(defaultValue ?? \"\");\n  const baseId = useId();\n  const reduce = useReducedMotion();\n  const current = value ?? inner;\n\n  const select = useCallback(\n    (next: string) => {\n      if (value === undefined) setInner(next);\n      onValueChange?.(next);\n    },\n    [value, onValueChange],\n  );\n\n  const ctx = useMemo(\n    () => ({ value: current, select, baseId, variant }),\n    [current, select, baseId, variant],\n  );\n\n  return (\n    <TabsContext.Provider value={ctx}>\n      <MotionConfig transition={reduce ? { duration: 0 } : INDICATOR_TRANSITION}>\n        {/* layoutRoot keeps the indicator measuring inside this component, so a\n            fixed or scrolled parent never turns scroll offset into movement. */}\n        <motion.div layoutRoot className={className}>\n          {children}\n        </motion.div>\n      </MotionConfig>\n    </TabsContext.Provider>\n  );\n}\n\nconst NAV_KEYS = new Set([\"ArrowRight\", \"ArrowLeft\", \"Home\", \"End\"]);\n\nexport function TabsList({ className, children }: { className?: string; children: ReactNode }) {\n  const { variant } = useTabs(\"TabsList\");\n\n  // Arrow keys, Home, and End move between tabs and select them.\n  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (!NAV_KEYS.has(event.key)) return;\n    const tabs = Array.from(\n      event.currentTarget.querySelectorAll<HTMLButtonElement>('[role=\"tab\"]:not(:disabled)'),\n    );\n    const index = tabs.indexOf(document.activeElement as HTMLButtonElement);\n    if (index < 0) return;\n    event.preventDefault();\n    const next =\n      event.key === \"Home\"\n        ? 0\n        : event.key === \"End\"\n          ? tabs.length - 1\n          : (index + (event.key === \"ArrowRight\" ? 1 : -1) + tabs.length) % tabs.length;\n    tabs[next].focus();\n    tabs[next].click();\n  };\n\n  return (\n    <div role=\"tablist\" onKeyDown={onKeyDown} className={cn(LIST_CLASS[variant], className)}>\n      {children}\n    </div>\n  );\n}\n\nexport interface TabsTriggerProps {\n  value: string;\n  className?: string;\n  indicatorClassName?: string;\n  children: ReactNode;\n}\n\nexport function TabsTrigger({ value, className, indicatorClassName, children }: TabsTriggerProps) {\n  const ctx = useTabs(\"TabsTrigger\");\n  const active = ctx.value === value;\n\n  return (\n    <button\n      type=\"button\"\n      role=\"tab\"\n      id={`${ctx.baseId}-tab-${value}`}\n      aria-selected={active}\n      aria-controls={`${ctx.baseId}-panel-${value}`}\n      tabIndex={active || !ctx.value ? 0 : -1}\n      onClick={() => ctx.select(value)}\n      className={cn(\n        \"relative inline-flex touch-manipulation items-center justify-center whitespace-nowrap text-sm font-medium outline-none transition-colors duration-150\",\n        \"focus-visible:ring-2 focus-visible:ring-foreground/40\",\n        TRIGGER_CLASS[ctx.variant],\n        active ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n        className,\n      )}\n    >\n      {active ? (\n        <motion.span\n          layoutId={`${ctx.baseId}-indicator`}\n          aria-hidden=\"true\"\n          className={cn(INDICATOR_CLASS[ctx.variant], indicatorClassName)}\n        />\n      ) : null}\n      <span className=\"relative\">{children}</span>\n    </button>\n  );\n}\n\nexport interface TabsContentProps {\n  value: string;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function TabsContent({ value, className, children }: TabsContentProps) {\n  const ctx = useTabs(\"TabsContent\");\n  const reduce = useReducedMotion();\n  const active = ctx.value === value;\n\n  // Inactive panels stay in the DOM, hidden, so their content is still in the\n  // server HTML. The active panel fades in.\n  return (\n    <motion.div\n      role=\"tabpanel\"\n      id={`${ctx.baseId}-panel-${value}`}\n      aria-labelledby={`${ctx.baseId}-tab-${value}`}\n      hidden={!active}\n      initial={false}\n      animate={{ opacity: active ? 1 : 0 }}\n      transition={{ duration: reduce ? 0 : 0.15, ease: EASE }}\n      className={cn(\"mt-4\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}