{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "highlight-tabs",
  "title": "Highlight Tabs",
  "description": "A Motion-powered tab list with a shared highlight indicator and keyboard navigation.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/base/ui/highlight-tabs.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { LayoutGroup, motion, useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type HighlightTab = {\n  value: string;\n  label: React.ReactNode;\n  disabled?: boolean;\n  /** Id applied to the trigger, so a tabpanel can point back via aria-labelledby. */\n  id?: string;\n  /** Id of the tabpanel this trigger controls. */\n  ariaControls?: string;\n};\n\nexport type HighlightTabsProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"defaultValue\" | \"onChange\"\n> & {\n  tabs: HighlightTab[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string, tab: HighlightTab) => void;\n  /**\n   * Commit selection when the pointer sweeps over a tab. Defaults to true —\n   * the moving highlight is this component's signature interaction. Set it to\n   * false when selection drives panel content, so hovering across the list\n   * doesn't churn `onValueChange` for every tab it crosses.\n   */\n  selectOnHover?: boolean;\n  listClassName?: string;\n  tabClassName?: string;\n  indicatorClassName?: string;\n  \"aria-label\"?: string;\n};\n\nfunction getEnabledTabs(tabs: HighlightTab[]) {\n  return tabs.filter((tab) => !tab.disabled);\n}\n\nfunction getInitialValue(tabs: HighlightTab[], value?: string) {\n  const enabledTabs = getEnabledTabs(tabs);\n\n  return enabledTabs.some((tab) => tab.value === value)\n    ? value\n    : enabledTabs[0]?.value;\n}\n\nexport function HighlightTabs({\n  tabs,\n  value,\n  defaultValue,\n  onValueChange,\n  selectOnHover = true,\n  className,\n  listClassName,\n  tabClassName,\n  indicatorClassName,\n  \"aria-label\": ariaLabel = \"Tabs\",\n  ...props\n}: HighlightTabsProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const reactId = React.useId();\n  const layoutId = `highlight-tabs-${reactId}`;\n  const isControlled = value !== undefined;\n  const tabRefs = React.useRef(new Map<string, HTMLButtonElement>());\n  const [uncontrolledValue, setUncontrolledValue] = React.useState(\n    () => getInitialValue(tabs, defaultValue) ?? \"\",\n  );\n\n  const enabledTabs = React.useMemo(() => getEnabledTabs(tabs), [tabs]);\n  const activeValue = isControlled\n    ? value\n    : enabledTabs.some((tab) => tab.value === uncontrolledValue)\n      ? uncontrolledValue\n      : enabledTabs[0]?.value;\n\n  const setActiveValue = React.useCallback(\n    (nextValue: string) => {\n      const nextTab = tabs.find((tab) => tab.value === nextValue);\n\n      if (!nextTab || nextTab.disabled || nextValue === activeValue) return;\n\n      if (!isControlled) {\n        setUncontrolledValue(nextValue);\n      }\n\n      onValueChange?.(nextValue, nextTab);\n    },\n    [activeValue, isControlled, onValueChange, tabs],\n  );\n\n  // A controlled `value` that matches no enabled tab (e.g. stale after `tabs`\n  // shrinks) must not leave every trigger at tabIndex -1, or the tablist\n  // becomes unreachable by keyboard.\n  const focusableValue = enabledTabs.some((tab) => tab.value === activeValue)\n    ? activeValue\n    : enabledTabs[0]?.value;\n\n  const focusTab = React.useCallback((nextValue: string) => {\n    tabRefs.current.get(nextValue)?.focus();\n  }, []);\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLButtonElement>, currentValue: string) => {\n      if (enabledTabs.length === 0) return;\n\n      const currentIndex = enabledTabs.findIndex(\n        (tab) => tab.value === currentValue,\n      );\n      const lastIndex = enabledTabs.length - 1;\n      let nextIndex = currentIndex;\n\n      if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n        nextIndex = currentIndex >= lastIndex ? 0 : currentIndex + 1;\n      } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n        nextIndex = currentIndex <= 0 ? lastIndex : currentIndex - 1;\n      } else if (event.key === \"Home\") {\n        nextIndex = 0;\n      } else if (event.key === \"End\") {\n        nextIndex = lastIndex;\n      } else {\n        return;\n      }\n\n      event.preventDefault();\n\n      const nextValue = enabledTabs[nextIndex]?.value;\n\n      if (!nextValue) return;\n\n      setActiveValue(nextValue);\n      focusTab(nextValue);\n    },\n    [enabledTabs, focusTab, setActiveValue],\n  );\n\n  const transition = shouldReduceMotion\n    ? { duration: 0 }\n    : ({ type: \"spring\", duration: 0.24, bounce: 0 } as const);\n\n  return (\n    <div\n      data-slot=\"highlight-tabs\"\n      className={cn(\"inline-flex\", className)}\n      {...props}\n    >\n      <LayoutGroup id={layoutId}>\n        <ul\n          role=\"tablist\"\n          aria-label={ariaLabel}\n          aria-orientation=\"horizontal\"\n          data-slot=\"highlight-tabs-list\"\n          className={cn(\n            \"inline-flex items-center gap-1 rounded-lg bg-muted/70 p-1\",\n            listClassName,\n          )}\n        >\n          {tabs.map((tab) => {\n            const isActive = activeValue === tab.value;\n\n            return (\n              <li key={tab.value} role=\"presentation\" className=\"relative\">\n                <button\n                  type=\"button\"\n                  role=\"tab\"\n                  id={tab.id}\n                  aria-controls={tab.ariaControls}\n                  aria-selected={isActive}\n                  disabled={tab.disabled}\n                  tabIndex={tab.value === focusableValue ? 0 : -1}\n                  ref={(element) => {\n                    if (element) {\n                      tabRefs.current.set(tab.value, element);\n                    } else {\n                      tabRefs.current.delete(tab.value);\n                    }\n                  }}\n                  data-slot=\"highlight-tabs-trigger\"\n                  data-active={isActive ? \"\" : undefined}\n                  onClick={() => setActiveValue(tab.value)}\n                  onFocus={() => setActiveValue(tab.value)}\n                  onPointerEnter={() => {\n                    if (selectOnHover) {\n                      setActiveValue(tab.value);\n                    }\n                  }}\n                  onKeyDown={(event) => handleKeyDown(event, tab.value)}\n                  className={cn(\n                    \"relative inline-flex h-8 items-center justify-center whitespace-nowrap rounded-md px-3 text-sm font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-active:text-foreground\",\n                    tabClassName,\n                  )}\n                >\n                  {isActive ? (\n                    <motion.span\n                      layoutId=\"highlight-tabs-indicator\"\n                      aria-hidden=\"true\"\n                      transition={transition}\n                      className={cn(\n                        \"absolute inset-0 rounded-md bg-background shadow-sm\",\n                        indicatorClassName,\n                      )}\n                    />\n                  ) : null}\n                  <span className=\"relative z-10\">{tab.label}</span>\n                </button>\n              </li>\n            );\n          })}\n        </ul>\n      </LayoutGroup>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/highlight-tabs.tsx"
    }
  ],
  "meta": {
    "tags": [
      "tab-list",
      "roving-focus",
      "arrow-keys",
      "indicator"
    ],
    "effects": [
      "shared-layout",
      "highlight"
    ]
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}