{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tabs","type":"registry:component","title":"Tabs","description":"Pill, segment or underline tabs with a spring layoutId indicator.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/tabs.tsx","type":"registry:component","target":"@components/motion/tabs.tsx","content":"\"use client\";\n\nimport { motion, MotionConfig, useReducedMotion, type Transition } from \"motion/react\";\nimport { createContext, useContext, useId, useState, type ReactNode } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype Variant = \"pill\" | \"underline\" | \"segment\";\n\ntype Ctx = {\n  value: string;\n  setValue: (v: string) => void;\n  layoutId: string;\n  variant: Variant;\n};\n\nconst TabsCtx = createContext<Ctx | null>(null);\n\nfunction useTabs() {\n  const ctx = useContext(TabsCtx);\n  if (!ctx) throw new Error(\"Tabs.* must be used inside <Tabs>\");\n  return ctx;\n}\n\n// Weighty spring — borrowed from dimi.me/lab/animated-tabs.\nconst transition: Transition = {\n  type: \"spring\",\n  stiffness: 170,\n  damping: 24,\n  mass: 1.2,\n};\n\nexport function Tabs({\n  defaultValue,\n  value,\n  onValueChange,\n  variant = \"pill\",\n  children,\n  className,\n}: {\n  defaultValue?: string;\n  value?: string;\n  onValueChange?: (v: string) => void;\n  variant?: Variant;\n  children: ReactNode;\n  className?: string;\n}) {\n  const [internal, setInternal] = useState(defaultValue ?? \"\");\n  const layoutId = useId();\n  const reduce = useReducedMotion();\n  const controlled = value !== undefined;\n  const current = controlled ? value : internal;\n  const setValue = (v: string) => {\n    if (!controlled) setInternal(v);\n    onValueChange?.(v);\n  };\n  return (\n    <MotionConfig transition={reduce ? { duration: 0 } : transition}>\n      <TabsCtx.Provider value={{ value: current, setValue, layoutId, variant }}>\n        {/* layoutRoot: the indicator's layoutId measures in page coordinates, so\n            inside fixed/scrolled containers it would replay scroll offsets as\n            movement. The pill only ever travels within the list, so scoping\n            projection to the Tabs wrapper is always correct. */}\n        <motion.div layoutRoot className={className}>\n          {children}\n        </motion.div>\n      </TabsCtx.Provider>\n    </MotionConfig>\n  );\n}\n\nconst listClasses: Record<Variant, string> = {\n  pill: \"inline-flex items-center gap-1 rounded-full bg-card p-1\",\n  underline: \"inline-flex items-center gap-1 border-b border-border\",\n  segment: \"inline-flex items-center gap-0 rounded-lg bg-card p-0.5\",\n};\n\nexport function TabsList({ children, className }: { children: ReactNode; className?: string }) {\n  const { variant } = useTabs();\n  return (\n    <div role=\"tablist\" className={cn(listClasses[variant], className)}>\n      {children}\n    </div>\n  );\n}\n\nexport function TabsTrigger({ value, children, className }: { value: string; children: ReactNode; className?: string }) {\n  const { value: current, setValue, layoutId, variant } = useTabs();\n  const active = current === value;\n\n  if (variant === \"underline\") {\n    return (\n      <button\n        type=\"button\"\n        role=\"tab\"\n        aria-selected={active}\n        onClick={() => setValue(value)}\n        className={cn(\n          \"relative isolate px-3 pb-2.5 pt-1 -mb-px text-sm font-medium transition-colors\",\n          active ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n          className,\n        )}\n      >\n        {children}\n        {active ? (\n          <motion.span\n            layoutId={layoutId}\n            className=\"absolute -bottom-px left-0 right-0 h-px bg-primary\"\n          />\n        ) : null}\n      </button>\n    );\n  }\n\n  // Pill + Segment use the same trick: a max-contrast pill slides via layoutId,\n  // text uses `mix-blend-exclusion` so it inverts dynamically against the moving bg.\n  const radius = variant === \"pill\" ? \"rounded-full\" : \"rounded-md\";\n\n  return (\n    <div className=\"relative\">\n      {active ? (\n        <motion.span\n          layoutId={layoutId}\n          style={{ borderRadius: variant === \"pill\" ? 9999 : 8 }}\n          className={cn(\n            \"absolute inset-0 bg-primary shadow-sm\",\n            radius,\n          )}\n        />\n      ) : null}\n      <button\n        type=\"button\"\n        role=\"tab\"\n        aria-selected={active}\n        onClick={() => setValue(value)}\n        className={cn(\n          \"relative z-10 inline-flex items-center justify-center whitespace-nowrap bg-transparent px-3.5 py-1.5 text-sm font-medium transition-colors outline-none\",\n          active ? \"text-primary-foreground\" : \"text-muted-foreground hover:text-foreground\",\n          radius,\n          className,\n        )}\n      >\n        {children}\n      </button>\n    </div>\n  );\n}\n\nexport function TabsContent({ value, children, className }: { value: string; children: ReactNode; className?: string }) {\n  const { value: current } = useTabs();\n  const reduce = useReducedMotion();\n  if (current !== value) return null;\n  return (\n    <motion.div\n      key={value}\n      initial={{ opacity: 0, y: reduce ? 0 : 4 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.18, ease: EASE_OUT }}\n      className={cn(\"mt-4\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\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"}]}