{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "tabs", "title": "Tabs", "description": "Tabbed interface for organizing content into selectable panels.", "dependencies": [ "@base-ui/react", "class-variance-authority", "lucide-react", "motion" ], "files": [ { "path": "packages/ui/src/tabs.tsx", "content": "\"use client\";\n\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n ChevronLeft as ChevronLeftIcon,\n ChevronRight as ChevronRightIcon,\n} from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport * as React from \"react\";\nimport { cn } from \"./utils\";\n\ntype TabsValue = string | number | null;\ntype TabsListVariant = \"default\" | \"line\";\n\nconst TabsActiveContext = React.createContext(null);\nconst TabsTriggerContext = React.createContext<{ isActive: boolean }>({\n isActive: false,\n});\n\n// Slightly bouncier + longer than the previous values so the FLIP scale on\n// the layoutId pill is spread across more frames — matches the v0 reference\n// and makes the mid-morph stretching less prominent.\nconst MORPH_SPRING = { type: \"spring\", duration: 0.4, bounce: 0.15 } as const;\n\n// =============================================================================\n// Tabs (Root)\n// =============================================================================\nexport interface TabsProps extends TabsPrimitive.Root.Props {}\n\nfunction Tabs({\n className,\n orientation = \"horizontal\",\n value,\n defaultValue,\n onValueChange,\n ...props\n}: TabsProps) {\n const [internalValue, setInternalValue] = React.useState(\n defaultValue ?? null\n );\n const activeValue = value ?? internalValue;\n\n const handleValueChange: TabsPrimitive.Root.Props[\"onValueChange\"] = (\n next,\n eventDetails\n ) => {\n setInternalValue(next);\n onValueChange?.(next, eventDetails);\n };\n\n return (\n \n \n \n );\n}\n\n// =============================================================================\n// TabsList\n// =============================================================================\nconst tabsListVariants = cva(\n \"group/tabs-list relative inline-flex w-fit items-center justify-center rounded-lg p-1 text-muted-foreground data-[shape=pill]:rounded-full data-[variant=line]:rounded-none group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col\",\n {\n variants: {\n variant: {\n default: \"bg-muted\",\n line: \"gap-1 bg-transparent\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\nconst TabsListContext = React.createContext<{\n morphing: boolean;\n variant: TabsListVariant;\n pillId: string;\n}>({\n morphing: false,\n variant: \"default\",\n pillId: \"\",\n});\n\nexport interface TabsListProps\n extends TabsPrimitive.List.Props,\n VariantProps {\n /**\n * Collapse inactive triggers to icon-only and reveal the label only on the\n * active tab. Each trigger should pair an icon with ``.\n */\n morphing?: boolean;\n /** Enable horizontal scroll with gradient fades and arrow navigation */\n scrollable?: boolean;\n /**\n * Shape of the tab list and its triggers\n * @default \"rounded\"\n */\n shape?: \"rounded\" | \"pill\";\n}\n\nfunction TabsList({\n className,\n variant = \"default\",\n shape = \"rounded\",\n scrollable,\n morphing = false,\n children,\n ...props\n}: TabsListProps) {\n const pillId = React.useId();\n const contextValue = React.useMemo(\n () => ({ morphing, variant: variant ?? \"default\", pillId }),\n [morphing, variant, pillId]\n );\n\n if (scrollable) {\n return (\n \n \n {children}\n \n \n );\n }\n\n // In morphing mode the active pill is a motion.span layoutId={pillId}\n // rendered inside the active TabsTrigger — suppress Base UI's CSS indicator.\n return (\n \n \n {!morphing && }\n {children}\n \n \n );\n}\n\n// =============================================================================\n// ScrollableTabsList (internal)\n// =============================================================================\n/** Pixels to scroll per arrow click in scrollable tabs */\nconst SCROLL_AMOUNT = 150;\n\nfunction ScrollableTabsList({\n className,\n variant = \"default\",\n shape = \"rounded\",\n morphing = false,\n children,\n ...props\n}: Omit) {\n const scrollRef = React.useRef(null);\n const [canScrollLeft, setCanScrollLeft] = React.useState(false);\n const [canScrollRight, setCanScrollRight] = React.useState(false);\n\n const updateScrollState = React.useCallback(() => {\n const el = scrollRef.current;\n if (!el) {\n return;\n }\n setCanScrollLeft(el.scrollLeft > 0);\n setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);\n }, []);\n\n React.useEffect(() => {\n const el = scrollRef.current;\n if (!el) {\n return;\n }\n\n updateScrollState();\n\n el.addEventListener(\"scroll\", updateScrollState, { passive: true });\n const observer = new ResizeObserver(updateScrollState);\n observer.observe(el);\n\n // Also observe the inner list so we detect when items are added/removed\n const list = el.firstElementChild;\n if (list) {\n observer.observe(list);\n }\n\n return () => {\n el.removeEventListener(\"scroll\", updateScrollState);\n observer.disconnect();\n };\n }, [updateScrollState]);\n\n const scrollLeft = () => {\n scrollRef.current?.scrollBy({ left: -SCROLL_AMOUNT, behavior: \"smooth\" });\n };\n\n const scrollRight = () => {\n scrollRef.current?.scrollBy({ left: SCROLL_AMOUNT, behavior: \"smooth\" });\n };\n\n return (\n
\n {canScrollLeft && (\n \n \n \n )}\n\n \n \n {!morphing && }\n {children}\n \n
\n\n {canScrollRight && (\n \n \n \n )}\n \n );\n}\n\n// =============================================================================\n// TabsTrigger\n// =============================================================================\nexport interface TabsTriggerProps extends TabsPrimitive.Tab.Props {}\n\nfunction TabsTrigger({\n className,\n children,\n value,\n ...props\n}: TabsTriggerProps) {\n const { morphing, variant, pillId } = React.useContext(TabsListContext);\n const activeValue = React.useContext(TabsActiveContext);\n const isActive = activeValue === value;\n const shouldReduceMotion = useReducedMotion();\n\n const triggerContext = React.useMemo(() => ({ isActive }), [isActive]);\n\n return (\n \n \n {morphing && isActive && (\n \n )}\n {children}\n \n \n );\n}\n\n// =============================================================================\n// TabsTriggerLabel\n// =============================================================================\nexport interface TabsTriggerLabelProps extends React.ComponentProps<\"span\"> {}\n\n/**\n * Wraps a tab's text label so it can collapse to width 0 when the parent\n * `TabsList` has `morphing` enabled and the trigger is not active. The active\n * tab springs from 0 to natural width via `motion`, and the icon→label\n * spacing lives inside the label so it animates together with the width.\n *\n * Outside `morphing` mode this renders a plain ``.\n */\nfunction TabsTriggerLabel({\n className,\n children,\n ...props\n}: TabsTriggerLabelProps) {\n const { morphing } = React.useContext(TabsListContext);\n const { isActive } = React.useContext(TabsTriggerContext);\n const shouldReduceMotion = useReducedMotion();\n\n if (!morphing) {\n return (\n \n {children}\n \n );\n }\n\n return (\n )}\n >\n {children}\n \n );\n}\n\n// =============================================================================\n// TabsIndicator\n// =============================================================================\nexport interface TabsIndicatorProps extends TabsPrimitive.Indicator.Props {}\n\nfunction TabsIndicator({ className, ...props }: TabsIndicatorProps) {\n return (\n \n );\n}\n\n// =============================================================================\n// TabsContent\n// =============================================================================\nexport interface TabsContentProps extends TabsPrimitive.Panel.Props {}\n\nfunction TabsContent({ className, ...props }: TabsContentProps) {\n return (\n \n );\n}\n\nexport {\n Tabs,\n TabsContent,\n TabsIndicator,\n TabsList,\n TabsTrigger,\n TabsTriggerLabel,\n tabsListVariants,\n};\n", "type": "registry:ui" } ], "type": "registry:ui" }