{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-tabs",
  "title": "Animated Tabs",
  "description": "Tabs with a gliding pill and panels stacked like cards that peek behind.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/animated-tabs.tsx",
      "content": "\"use client\";\n\nimport { useId, useRef, useState } from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface Tab {\n  title: React.ReactNode;\n  value: string;\n  content: React.ReactNode;\n}\n\ninterface AnimatedTabsProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"defaultValue\"> {\n  /** The tabs, in order */\n  tabs: Tab[];\n  /** Value of the tab selected on first render (defaults to the first tab) */\n  defaultValue?: string;\n  /** Controlled selected value */\n  value?: string;\n  /** Called with the new value when the selection changes */\n  onValueChange?: (value: string) => void;\n  /** How many inactive panels peek out behind the active one */\n  peek?: number;\n  /** Classes for the tab list */\n  listClassName?: string;\n  /** Classes for every panel card */\n  panelClassName?: string;\n}\n\n/**\n * Tabs with a pill that glides to the selected tab and panels stacked like\n * cards: the next panels peek out behind the active one, scaled and offset.\n * Follows the WAI-ARIA tabs pattern with automatic activation.\n */\nexport function AnimatedTabs({\n  tabs,\n  defaultValue,\n  value: valueProp,\n  onValueChange,\n  peek = 2,\n  listClassName,\n  panelClassName,\n  className,\n  ...props\n}: AnimatedTabsProps) {\n  const [inner, setInner] = useState(defaultValue ?? tabs[0]?.value);\n  const value = valueProp ?? inner;\n  const reduce = useReducedMotion();\n  const id = useId();\n  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const active = Math.max(0, tabs.findIndex((t) => t.value === value));\n  const spring = reduce\n    ? { duration: 0 }\n    : { type: \"spring\" as const, stiffness: 360, damping: 32 };\n\n  const select = (index: number) => {\n    const tab = tabs[index];\n    if (!tab) return;\n    if (valueProp === undefined) setInner(tab.value);\n    onValueChange?.(tab.value);\n  };\n\n  const onKeyDown = (event: React.KeyboardEvent) => {\n    const n = tabs.length;\n    const next = {\n      ArrowRight: (active + 1) % n,\n      ArrowLeft: (active - 1 + n) % n,\n      Home: 0,\n      End: n - 1,\n    }[event.key];\n    if (next === undefined) return;\n    event.preventDefault();\n    select(next);\n    tabRefs.current[next]?.focus();\n  };\n\n  return (\n    <div {...props} data-slot=\"animated-tabs\" className={cn(\"flex w-full flex-col gap-6\", className)}>\n      <div\n        role=\"tablist\"\n        aria-orientation=\"horizontal\"\n        onKeyDown={onKeyDown}\n        className={cn(\n          \"flex max-w-full gap-1 self-start overflow-x-auto rounded-full border bg-muted/50 p-1\",\n          listClassName\n        )}\n      >\n        {tabs.map((tab, i) => (\n          <button\n            key={tab.value}\n            ref={(el) => {\n              tabRefs.current[i] = el;\n            }}\n            type=\"button\"\n            role=\"tab\"\n            id={`${id}-tab-${i}`}\n            aria-selected={i === active}\n            aria-controls={`${id}-panel-${i}`}\n            tabIndex={i === active ? 0 : -1}\n            onClick={() => select(i)}\n            className=\"relative shrink-0 cursor-pointer rounded-full px-4 py-1.5 text-sm font-medium whitespace-nowrap text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-selected:text-foreground\"\n          >\n            {i === active && (\n              <motion.span\n                layoutId={`${id}-pill`}\n                transition={spring}\n                aria-hidden\n                className=\"absolute inset-0 rounded-full border bg-card shadow-sm\"\n              />\n            )}\n            <span className=\"relative\">{tab.title}</span>\n          </button>\n        ))}\n      </div>\n\n      {/* Every panel shares one grid cell, so the stack is as tall as the\n          tallest panel; room above it is left for the peeking cards. */}\n      <div className=\"grid\" style={{ paddingTop: Math.min(peek, tabs.length - 1) * 14 }}>\n        {tabs.map((tab, i) => {\n          const depth = (i - active + tabs.length) % tabs.length;\n          const current = depth === 0;\n          return (\n            <motion.div\n              key={tab.value}\n              role=\"tabpanel\"\n              id={`${id}-panel-${i}`}\n              aria-labelledby={`${id}-tab-${i}`}\n              aria-hidden={!current || undefined}\n              inert={!current || undefined}\n              tabIndex={current ? 0 : undefined}\n              initial={false}\n              animate={{\n                y: -depth * 14,\n                scale: 1 - depth * 0.05,\n                opacity: depth > peek ? 0 : 1,\n              }}\n              transition={spring}\n              style={{ zIndex: tabs.length - depth, transformOrigin: \"top center\" }}\n              className={cn(\n                \"col-start-1 row-start-1 overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-lg shadow-foreground/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                panelClassName\n              )}\n            >\n              {tab.content}\n            </motion.div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/animated-tabs.tsx"
    }
  ],
  "type": "registry:ui"
}