{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "title": "Tabs",
  "description": "Tab list with a spring-driven sliding indicator, proximity hover, and a directional panel handoff.",
  "dependencies": [
    "motion",
    "@base-ui/react"
  ],
  "registryDependencies": [
    "utils",
    "springs",
    "font-weight",
    "use-proximity-hover"
  ],
  "files": [
    {
      "path": "src/components/ui/tabs.tsx",
      "content": "\"use client\";\n\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ReactElement,\n} from \"react\";\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport { motion, AnimatePresence, useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { spring } from \"@/lib/springs\";\nimport { fontWeights } from \"@/lib/font-weight\";\nimport {\n  useProximityHover,\n  proximityHoverWashClassName,\n  proximityHoverWashOpacity,\n  type ItemRect,\n} from \"@/hooks/use-proximity-hover\";\n\n// ─── Contexts ────────────────────────────────────────────────────────────────\n// Base UI doesn't expose a public hook for \"which value is active\" to\n// arbitrary descendants (only its own Root/List/Tab/Panel/Indicator\n// components can see that internally) — TabsList needs it to resolve the\n// selected tab's index into `itemRects`, so Tabs tracks it itself instead.\n\ninterface TabsValueOrderContextValue {\n  valueOrder: string[];\n  setValueOrder: (order: string[]) => void;\n  selectedValue: string | undefined;\n}\n\nconst TabsValueOrderContext = createContext<TabsValueOrderContextValue | null>(null);\n\ninterface TabsListContextValue {\n  registerTab: (index: number, element: HTMLElement | null) => void;\n  hoveredIndex: number | null;\n  selectedValue: string | undefined;\n  /** Optimistically set on click so the indicator jumps immediately, without waiting for the controlled value to round-trip back. */\n  setOptimisticIndex: (index: number) => void;\n}\n\nconst TabsListContext = createContext<TabsListContextValue | null>(null);\n\nfunction useTabsListContext() {\n  const ctx = useContext(TabsListContext);\n  if (!ctx) throw new Error(\"TabsTrigger must be used within a TabsList\");\n  return ctx;\n}\n\n// ─── Tabs ────────────────────────────────────────────────────────────────────\n\nfunction Tabs({\n  value,\n  onValueChange,\n  defaultValue,\n  children,\n  className,\n  ...props\n}: TabsPrimitive.Root.Props) {\n  const [valueOrder, setValueOrder] = useState<string[]>([]);\n  const [uncontrolledValue, setUncontrolledValue] = useState<unknown>(defaultValue);\n\n  const updateValueOrder = useCallback((order: string[]) => {\n    setValueOrder((current) =>\n      current.length === order.length && current.every((v, i) => v === order[i]) ? current : order,\n    );\n  }, []);\n\n  const resolvedValue = value ?? uncontrolledValue ?? valueOrder[0];\n\n  // Base UI passes (value, eventDetails) — only the value matters here.\n  const handleValueChange = useCallback(\n    (newValue: unknown, eventDetails: unknown) => {\n      if (value === undefined) setUncontrolledValue(newValue);\n      (onValueChange as ((v: unknown, e: unknown) => void) | undefined)?.(newValue, eventDetails);\n    },\n    [onValueChange, value],\n  );\n\n  return (\n    <TabsValueOrderContext.Provider\n      value={{\n        valueOrder,\n        setValueOrder: updateValueOrder,\n        selectedValue: resolvedValue as string | undefined,\n      }}\n    >\n      {/*\n        Always controlled: Base UI's useControlled warns in dev when value\n        flips undefined → defined. valueOrder is empty on the first commit,\n        so fall back to an empty-string sentinel — TabsList's layout effect\n        populates valueOrder pre-paint, so the corrected value lands before\n        anything is visible.\n      */}\n      {/* grid, not flex-col: Base UI mounts the incoming panel before\n          unmounting the outgoing one, so for one paint frame both panels are\n          in the DOM at once. In a flex column that briefly doubles the\n          block's height (both panels stacked) — invisible on its own, but\n          enough to visibly nudge anything that vertically centers this\n          block against a fixed-height box (see docs/design-system.md\n          \"Preview-grid tile pattern\"), which then bleeds into an in-flight\n          layout animation like the selected-tab pill's slide. Explicitly\n          placing every TabsContent in the same grid cell (below) means an\n          overlapping pair shares space instead of stacking, so that\n          transient frame never changes the block's height at all — same\n          fix TabsTrigger's own ghost-span already uses for width. */}\n      <TabsPrimitive.Root\n        data-slot=\"tabs\"\n        value={resolvedValue ?? \"\"}\n        onValueChange={handleValueChange}\n        className={cn(\"grid gap-2\", className)}\n        {...props}\n      >\n        {children}\n      </TabsPrimitive.Root>\n    </TabsValueOrderContext.Provider>\n  );\n}\n\n// ─── TabsList ────────────────────────────────────────────────────────────────\n// Owns the sliding \"selected\" pill and the proximity hover pill — the same\n// measured-rect pattern Accordion's item-highlight uses, applied along the x\n// axis since tabs lay out horizontally.\n\nfunction TabsList({ children, className, ...props }: TabsPrimitive.List.Props) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const isMouseInsideRef = useRef(false);\n  const valueOrderCtx = useContext(TabsValueOrderContext);\n  const [optimisticIndex, setOptimisticIndex] = useState<number | null>(null);\n\n  const values = Children.toArray(children)\n    .filter(isValidElement)\n    .map((child) => (child.props as { value?: string }).value)\n    .filter((v): v is string => typeof v === \"string\");\n  const valueOrderKey = values.join(\",\");\n  const setValueOrder = valueOrderCtx?.setValueOrder;\n\n  useLayoutEffect(() => {\n    setValueOrder?.(values);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [setValueOrder, valueOrderKey]);\n\n  const {\n    activeIndex: hoveredIndex,\n    setActiveIndex: setHoveredIndex,\n    itemRects,\n    handlers,\n    registerItem,\n    measureItems,\n  } = useProximityHover(containerRef, { axis: \"x\" });\n\n  useEffect(() => {\n    measureItems();\n  }, [measureItems, children]);\n\n  const handleMouseMove = useCallback(\n    (e: React.MouseEvent) => {\n      isMouseInsideRef.current = true;\n      handlers.onMouseMove(e);\n    },\n    [handlers],\n  );\n\n  const handleMouseLeave = useCallback(() => {\n    isMouseInsideRef.current = false;\n    handlers.onMouseLeave();\n  }, [handlers]);\n\n  const selectedValue = valueOrderCtx?.selectedValue;\n  const selectedIndex = selectedValue !== undefined ? values.indexOf(selectedValue) : -1;\n\n  useEffect(() => {\n    setOptimisticIndex(selectedIndex >= 0 ? selectedIndex : null);\n  }, [selectedIndex]);\n\n  const selectedRect: ItemRect | null =\n    optimisticIndex !== null ? (itemRects[optimisticIndex] ?? null) : null;\n  const hoverRect: ItemRect | null =\n    hoveredIndex !== null ? (itemRects[hoveredIndex] ?? null) : null;\n  const isHoveringSelected = hoveredIndex === optimisticIndex;\n  const isHovering = hoveredIndex !== null && !isHoveringSelected;\n\n  // Auto-index children so callers never hand-thread an index just for\n  // proximity hover/rect tracking — mirrors Accordion's indexedChildren.\n  const indexedChildren = Children.map(children, (child, index) => {\n    if (!isValidElement(child)) return child;\n    return cloneElement(child as ReactElement<{ _index?: number }>, { _index: index });\n  });\n\n  return (\n    <TabsListContext.Provider\n      value={{\n        registerTab: registerItem,\n        hoveredIndex,\n        selectedValue,\n        setOptimisticIndex,\n      }}\n    >\n      <TabsPrimitive.List\n        data-slot=\"tabs-list\"\n        ref={(node: HTMLDivElement | null) => {\n          (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;\n        }}\n        onMouseMove={handleMouseMove}\n        onMouseEnter={handlers.onMouseEnter}\n        onMouseLeave={handleMouseLeave}\n        onFocus={(e: React.FocusEvent) => {\n          const trigger = (e.target as HTMLElement).closest(\"[data-proximity-index]\");\n          const indexAttr = trigger?.getAttribute(\"data-proximity-index\");\n          if (indexAttr != null) setHoveredIndex(Number(indexAttr));\n        }}\n        onBlur={(e: React.FocusEvent) => {\n          if (containerRef.current?.contains(e.relatedTarget as Node)) return;\n          if (!isMouseInsideRef.current) setHoveredIndex(null);\n        }}\n        className={cn(\n          \"relative col-start-1 row-start-1 inline-flex w-fit items-center gap-0.5 rounded-lg bg-background p-1 text-muted-foreground shadow-well\",\n          className,\n        )}\n        {...props}\n      >\n        {selectedRect && (\n          <motion.div\n            layout\n            className=\"pointer-events-none absolute rounded-md bg-card shadow-bevel\"\n            style={{\n              left: selectedRect.left,\n              top: selectedRect.top,\n              width: selectedRect.width,\n              height: selectedRect.height,\n            }}\n            initial={false}\n            animate={{ opacity: isHovering ? 0.85 : 1 }}\n            transition={{ ...spring.moderate.enter, opacity: { duration: 0.08 } }}\n          />\n        )}\n\n        <AnimatePresence>\n          {hoverRect && !isHoveringSelected && selectedRect && (\n            <motion.div\n              layout\n              className={cn(\"pointer-events-none absolute rounded-md\", proximityHoverWashClassName)}\n              style={{\n                left: hoverRect.left,\n                top: hoverRect.top,\n                width: hoverRect.width,\n                height: hoverRect.height,\n              }}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: proximityHoverWashOpacity }}\n              exit={{ opacity: 0, transition: spring.fast.exit }}\n              transition={spring.fast.enter}\n            />\n          )}\n        </AnimatePresence>\n\n        {indexedChildren}\n      </TabsPrimitive.List>\n    </TabsListContext.Provider>\n  );\n}\n\n// ─── TabsTrigger ─────────────────────────────────────────────────────────────\n\nfunction TabsTrigger({\n  className,\n  children,\n  onClick,\n  _index = 0,\n  ...props\n}: TabsPrimitive.Tab.Props & { _index?: number }) {\n  const { registerTab, hoveredIndex, selectedValue, setOptimisticIndex } = useTabsListContext();\n  const ref = useRef<HTMLElement>(null);\n\n  // useLayoutEffect, not useEffect: pairs with useProximityHover's\n  // registration-tick effect so the selected pill is measured and painted in\n  // the same pre-paint commit as mount, instead of popping in a frame later.\n  useLayoutEffect(() => {\n    registerTab(_index, ref.current);\n    return () => registerTab(_index, null);\n  }, [_index, registerTab]);\n\n  const isSelected = selectedValue === props.value;\n  const isActive = hoveredIndex === _index || isSelected;\n\n  return (\n    <TabsPrimitive.Tab\n      ref={ref}\n      data-slot=\"tabs-trigger\"\n      data-proximity-index={_index}\n      // Composed, not spread-overridable: a consumer onClick must not\n      // replace the optimistic indicator jump.\n      onClick={(e) => {\n        setOptimisticIndex(_index);\n        onClick?.(e);\n      }}\n      className={cn(\n        \"relative z-10 inline-flex h-8 items-center justify-center rounded-md px-3 whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      {/* Ghost-span: an invisible copy at the heaviest weight reserves the\n          width so the visible copy's weight can animate without reflowing\n          the tab. Each stacked copy is its own flex row (not the outer Tab)\n          so an icon + label child pair still lays out side by side within\n          each copy. */}\n      <span className=\"col-start-1 row-start-1 grid text-control\">\n        <span\n          className=\"invisible col-start-1 row-start-1 inline-flex items-center gap-1.5\"\n          style={{ fontVariationSettings: fontWeights.medium }}\n          aria-hidden=\"true\"\n        >\n          {children}\n        </span>\n        <span\n          className={cn(\n            \"col-start-1 row-start-1 inline-flex items-center gap-1.5 transition-colors duration-fast\",\n            isActive ? \"text-foreground\" : \"text-muted-foreground\",\n          )}\n          style={{ fontVariationSettings: isSelected ? fontWeights.medium : fontWeights.normal }}\n        >\n          {children}\n        </span>\n      </span>\n    </TabsPrimitive.Tab>\n  );\n}\n\n// ─── TabsContent ─────────────────────────────────────────────────────────────\n\nfunction TabsContent({ className, render: _render, ...props }: TabsPrimitive.Panel.Props) {\n  const reduceMotion = useReducedMotion();\n\n  return (\n    <TabsPrimitive.Panel\n      {...props}\n      render={(panelProps, state) => {\n        const exiting = state.transitionStatus === \"ending\";\n        const offset = reduceMotion\n          ? { x: 0, y: 0 }\n          : {\n              x:\n                state.tabActivationDirection === \"right\"\n                  ? 4\n                  : state.tabActivationDirection === \"left\"\n                    ? -4\n                    : 0,\n              y:\n                state.tabActivationDirection === \"down\"\n                  ? 4\n                  : state.tabActivationDirection === \"up\"\n                    ? -4\n                    : 0,\n            };\n\n        return (\n          <motion.div\n            {...(panelProps as Record<string, unknown>)}\n            data-slot=\"tabs-content\"\n            // min-w-0: grid items default to min-width:auto, so an unbreakable\n            // child (the code block's <pre>, which never wraps) pushes the\n            // implicit grid column — and with it this whole tab card — wider than\n            // its container instead of triggering the pre's own overflow-x.\n            className={cn(\"col-start-1 row-start-2 min-w-0 outline-none\", className)}\n            initial={{ opacity: 0, ...offset }}\n            animate={{\n              opacity: exiting ? 0 : 1,\n              x: exiting ? -offset.x : 0,\n              y: exiting ? -offset.y : 0,\n            }}\n            transition={exiting ? spring.quick.exit : spring.moderate.enter}\n          />\n        );\n      }}\n    />\n  );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}