{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Animated tabs component with sliding indicator","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useCallback, useId, useState } from \"react\";\n\nexport interface AnimatedTabsProps {\n  activeTab?: string;\n  className?: string;\n  defaultTab?: string;\n  layoutId?: string;\n  onChange?: (tabId: string) => void;\n  tabs: { id: string; label: string; icon?: ReactNode }[];\n  variant?: \"underline\" | \"pill\" | \"segment\";\n}\n\nconst SPRING = {\n  bounce: 0.05,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nexport default function AnimatedTabs({\n  tabs,\n  activeTab: controlledActiveTab,\n  defaultTab,\n  onChange,\n  variant = \"underline\",\n  layoutId: customLayoutId,\n  className,\n}: AnimatedTabsProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const generatedId = useId();\n  const layoutId = customLayoutId ?? `animated-tabs-${generatedId}`;\n\n  const [internalActiveTab, setInternalActiveTab] = useState(\n    defaultTab ?? tabs[0]?.id ?? \"\"\n  );\n\n  const isControlled = controlledActiveTab !== undefined;\n  const activeTab = isControlled ? controlledActiveTab : internalActiveTab;\n\n  const handleTabChange = useCallback(\n    (tabId: string) => {\n      if (!isControlled) {\n        setInternalActiveTab(tabId);\n      }\n      onChange?.(tabId);\n    },\n    [isControlled, onChange]\n  );\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent, currentIndex: number) => {\n      let newIndex = currentIndex;\n\n      if (event.key === \"ArrowRight\") {\n        event.preventDefault();\n        newIndex = (currentIndex + 1) % tabs.length;\n      } else if (event.key === \"ArrowLeft\") {\n        event.preventDefault();\n        newIndex = (currentIndex - 1 + tabs.length) % tabs.length;\n      } else if (event.key === \"Home\") {\n        event.preventDefault();\n        newIndex = 0;\n      } else if (event.key === \"End\") {\n        event.preventDefault();\n        newIndex = tabs.length - 1;\n      } else {\n        return;\n      }\n\n      const newTab = tabs[newIndex];\n      if (newTab) {\n        handleTabChange(newTab.id);\n        const tabElement = document.getElementById(\n          `${layoutId}-tab-${newTab.id}`\n        );\n        tabElement?.focus();\n      }\n    },\n    [tabs, handleTabChange, layoutId]\n  );\n\n  const baseContainerStyles = cn(\n    \"relative inline-flex\",\n    variant === \"underline\" && \"gap-1 border-border border-b\",\n    variant === \"pill\" && \"gap-1 rounded-full bg-muted p-1\",\n    variant === \"segment\" && \"gap-0 rounded-lg bg-muted p-1\"\n  );\n\n  const getTabStyles = (isActive: boolean) =>\n    cn(\n      \"relative z-10 flex cursor-pointer items-center justify-center gap-2 px-4 py-2 font-medium text-sm transition-colors\",\n      \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      variant === \"underline\" && [\n        \"rounded-t-md\",\n        isActive\n          ? \"text-foreground\"\n          : \"text-muted-foreground hover:text-foreground\",\n      ],\n      variant === \"pill\" && [\n        \"rounded-full\",\n        isActive\n          ? \"text-foreground\"\n          : \"text-muted-foreground hover:text-foreground\",\n      ],\n      variant === \"segment\" && [\n        \"flex-1 rounded-md\",\n        isActive\n          ? \"text-foreground\"\n          : \"text-muted-foreground hover:text-foreground\",\n      ]\n    );\n\n  const getIndicatorStyles = () =>\n    cn(\n      \"absolute\",\n      variant === \"underline\" && \"right-0 -bottom-px left-0 h-0.5 bg-brand\",\n      variant === \"pill\" &&\n        \"inset-0 rounded-full border border-border bg-background shadow-sm\",\n      variant === \"segment\" &&\n        \"inset-0 rounded-md border border-border bg-background shadow-sm\"\n    );\n\n  return (\n    <div\n      aria-label=\"Tabs\"\n      className={cn(baseContainerStyles, className)}\n      role=\"tablist\"\n    >\n      {tabs.map((tab, index) => {\n        const isActive = activeTab === tab.id;\n\n        return (\n          <button\n            aria-selected={isActive}\n            className={getTabStyles(isActive)}\n            id={`${layoutId}-tab-${tab.id}`}\n            key={tab.id}\n            onClick={() => handleTabChange(tab.id)}\n            onKeyDown={(e) => handleKeyDown(e, index)}\n            role=\"tab\"\n            tabIndex={isActive ? 0 : -1}\n            type=\"button\"\n          >\n            {isActive && (\n              <motion.span\n                className={getIndicatorStyles()}\n                layout\n                layoutId={layoutId}\n                style={{ originY: \"0px\" }}\n                transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n              />\n            )}\n            {tab.icon ? (\n              <span className=\"relative z-10\">{tab.icon}</span>\n            ) : null}\n            <span className=\"relative z-10\">{tab.label}</span>\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/animated-tabs/index.tsx","type":"registry:ui"}],"name":"animated-tabs","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Animated Tabs","type":"registry:ui"}