{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "expandable-tabs",
  "title": "Expandable Tabs",
  "description": "Animated icon tabs with a sliding active pill and an expanding label, built on the shadcn tooltip.",
  "registryDependencies": ["tooltip"],
  "files": [
    {
      "path": "components/ui/expandable-tabs.tsx",
      "content": "\"use client\";\n\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ExpandableTabsItem {\n  value: string;\n  label: string;\n  icon: ReactNode;\n  href?: string;\n}\n\ninterface ExpandableTabsProps {\n  items: ExpandableTabsItem[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  label?: string;\n  className?: string;\n}\n\ninterface ExpandableTabProps {\n  item: ExpandableTabsItem;\n  isActive: boolean;\n  onSelect: (value: string) => void;\n}\n\nfunction ExpandableTab({ item, isActive, onSelect }: ExpandableTabProps) {\n  const tabClassName =\n    \"relative z-10 flex shrink-0 cursor-pointer items-center rounded-[calc(var(--radius-2xl)-6px)] p-2 outline-none transition-colors duration-200 ease-out focus-visible:ring-2 focus-visible:ring-ring\";\n\n  const content = (\n    <>\n      {item.icon}\n      <span\n        className={cn(\n          \"grid transition-[grid-template-columns,opacity] duration-300 ease-out\",\n          isActive ? \"grid-cols-[1fr] opacity-100\" : \"grid-cols-[0fr] opacity-0\"\n        )}\n      >\n        <span className=\"min-w-0 overflow-hidden whitespace-nowrap font-medium text-foreground text-sm\">\n          <span className=\"block w-max pr-1 pl-2\">{item.label}</span>\n        </span>\n      </span>\n    </>\n  );\n\n  return (\n    <Tooltip disabled={isActive}>\n      <TooltipTrigger\n        render={\n          item.href ? (\n            <a\n              aria-current={isActive ? \"page\" : undefined}\n              className={tabClassName}\n              data-value={item.value}\n              href={item.href}\n            >\n              {content}\n            </a>\n          ) : (\n            <button\n              aria-pressed={isActive}\n              className={tabClassName}\n              data-value={item.value}\n              onClick={() => onSelect(item.value)}\n              type=\"button\"\n            >\n              {content}\n            </button>\n          )\n        }\n      />\n      <TooltipContent>{item.label}</TooltipContent>\n    </Tooltip>\n  );\n}\n\nfunction useActivePill(activeValue: string | undefined) {\n  const menuRef = useRef<HTMLMenuElement>(null);\n  const [pill, setPill] = useState<{\n    left: number;\n    top: number;\n    width: number;\n    height: number;\n  } | null>(null);\n\n  const measure = useCallback(() => {\n    const menu = menuRef.current;\n    if (!menu || activeValue === undefined) {\n      setPill(null);\n      return;\n    }\n    const active = menu.querySelector<HTMLElement>(\n      `[data-value=\"${CSS.escape(activeValue)}\"]`\n    );\n    if (!active) {\n      setPill(null);\n      return;\n    }\n    setPill({\n      left: active.offsetLeft,\n      top: active.offsetTop,\n      width: active.offsetWidth,\n      height: active.offsetHeight,\n    });\n  }, [activeValue]);\n\n  useLayoutEffect(() => {\n    measure();\n  }, [measure]);\n\n  useEffect(() => {\n    const menu = menuRef.current;\n    if (!menu) {\n      return;\n    }\n    const observer = new ResizeObserver(measure);\n    observer.observe(menu);\n    for (const tab of menu.querySelectorAll(\"[data-value]\")) {\n      observer.observe(tab);\n    }\n    return () => observer.disconnect();\n  }, [measure]);\n\n  return { menuRef, pill };\n}\n\nexport function ExpandableTabs({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  label = \"Choose an option\",\n  className,\n}: ExpandableTabsProps) {\n  const [internalValue, setInternalValue] = useState(\n    defaultValue ?? items[0]?.value\n  );\n  const activeValue = value ?? internalValue;\n  const { menuRef, pill } = useActivePill(activeValue);\n\n  const handleSelect = (next: string) => {\n    setInternalValue(next);\n    onValueChange?.(next);\n  };\n\n  return (\n    <div className=\"flex justify-center\">\n      <menu\n        aria-label={label}\n        className={cn(\n          \"relative m-0 flex max-w-full list-none items-center gap-1.5 overflow-x-auto overscroll-none rounded-2xl border bg-muted/50 p-1.5 [scrollbar-width:none] sm:flex-wrap sm:justify-center sm:overflow-visible [&::-webkit-scrollbar]:hidden\",\n          className\n        )}\n        ref={menuRef}\n      >\n        {pill && (\n          <span\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute rounded-[calc(var(--radius-2xl)-6px)] bg-background shadow-sm ring-1 ring-border transition-[left,top,width,height] duration-300 ease-out\"\n            style={{\n              left: pill.left,\n              top: pill.top,\n              width: pill.width,\n              height: pill.height,\n            }}\n          />\n        )}\n        {items.map((item) => (\n          <ExpandableTab\n            isActive={item.value === activeValue}\n            item={item}\n            key={item.value}\n            onSelect={handleSelect}\n          />\n        ))}\n      </menu>\n    </div>\n  );\n}\n\nexport type { ExpandableTabsItem, ExpandableTabsProps };\n",
      "type": "registry:ui",
      "target": "@ui/expandable-tabs.tsx"
    }
  ],
  "type": "registry:ui"
}
