{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "workspace-tabs",
  "title": "WorkspaceTabs",
  "description": "Workspace tab bar with gliding underline, per-tab action slot, and pane crossfade.",
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/neon-tokens.json",
    "https://ui.neon.com/r/tabs.json"
  ],
  "files": [
    {
      "path": "src/components/workspace-tabs/workspace-tabs.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface WorkspaceTab {\n  /** Stable id, doubles as the panel value, e.g. \"preview\". */\n  id: string;\n  /** Mono label, e.g. \"preview\". */\n  label: string;\n  /** Optional leading icon. */\n  icon?: ReactNode;\n  /** Optional count rendered after the label, e.g. checkpoints. */\n  count?: number;\n  /** Right-aligned actions shown only while this tab is active. */\n  actions?: ReactNode;\n  /** The pane content. */\n  content: ReactNode;\n  disabled?: boolean;\n}\n\nexport type WorkspaceTabsProps = Omit<\n  ComponentProps<typeof Tabs>,\n  \"children\"\n> & {\n  tabs: WorkspaceTab[];\n  /** Controlled active tab id. */\n  value?: string;\n  /** Uncontrolled initial tab id; defaults to the first tab. */\n  defaultValue?: string;\n  onValueChange?: (id: string) => void;\n};\n\n/* ─────────────────────────────────────────────────────────\n * TAB BAR STORYBOARD\n *\n *  rest     mono lowercase labels, muted; the active tab\n *           holds foreground with a primary underline\n *  switch   the underline glides to the new tab (240ms\n *           strong ease-out, transform-only) and the new\n *           pane fades up 4px; the old pane just leaves\n *  actions  each tab owns a right-aligned action slot that\n *           crossfades with the tab switch — the bar's\n *           height never changes\n *  hover    label warms to foreground, nothing moves\n * ───────────────────────────────────────────────────────── */\nconst EASE_OUT = \"cubic-bezier(0.23, 1, 0.32, 1)\";\n\n/** The underline that glides between active tabs. */\nconst GlideUnderline = ({ activeId }: { activeId: string }) => {\n  const ref = useRef<HTMLSpanElement>(null);\n  const [rect, setRect] = useState<{ left: number; width: number } | null>(\n    null\n  );\n\n  useEffect(() => {\n    const list = ref.current?.closest('[data-slot=\"tabs-list\"]');\n\n    if (!(list instanceof HTMLElement)) {\n      return;\n    }\n\n    const measure = () => {\n      const active = list.querySelector<HTMLElement>(\"[data-active]\");\n\n      if (active) {\n        setRect({ left: active.offsetLeft, width: active.offsetWidth });\n      }\n    };\n\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(list);\n    return () => observer.disconnect();\n  }, [activeId]);\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"pointer-events-none absolute bottom-0 h-0.5 bg-primary motion-reduce:transition-none\",\n        rect ? \"opacity-100\" : \"opacity-0\"\n      )}\n      ref={ref}\n      style={{\n        left: rect?.left ?? 0,\n        transition: `left 240ms ${EASE_OUT}, width 240ms ${EASE_OUT}`,\n        width: rect?.width ?? 0,\n      }}\n    />\n  );\n};\n\nexport const WorkspaceTabs = ({\n  className,\n  defaultValue,\n  onValueChange,\n  tabs,\n  value,\n  ...props\n}: WorkspaceTabsProps) => {\n  const fallback = defaultValue ?? tabs[0]?.id;\n  const [internal, setInternal] = useState(fallback);\n  const active = value ?? internal;\n  const activeTab = tabs.find((tab) => tab.id === active);\n\n  const handleChange = (next: unknown) => {\n    const id = String(next);\n    setInternal(id);\n    onValueChange?.(id);\n  };\n\n  return (\n    <Tabs\n      className={cn(\"gap-0\", className)}\n      data-slot=\"workspace-tabs\"\n      onValueChange={handleChange}\n      value={active}\n      {...props}\n    >\n      <div className=\"flex items-center justify-between gap-3 border-border/40 border-b\">\n        <TabsList\n          className=\"relative h-9 gap-1 rounded-none bg-transparent p-0\"\n          variant=\"line\"\n        >\n          {tabs.map((tab) => (\n            <TabsTrigger\n              className=\"h-full flex-none rounded-none px-3 font-mono text-muted-foreground text-xs after:hidden hover:text-foreground data-active:bg-transparent data-active:text-foreground dark:data-active:border-transparent dark:data-active:bg-transparent\"\n              disabled={tab.disabled}\n              key={tab.id}\n              value={tab.id}\n            >\n              {tab.icon}\n              {tab.label}\n              {typeof tab.count === \"number\" ? (\n                <span\n                  className=\"inline-flex h-3.5 min-w-3.5 items-center justify-center rounded-sm border border-border/60 px-[3px] pt-px text-[10px] text-muted-foreground leading-none tabular-nums transition-colors\"\n                  data-slot=\"workspace-tabs-count\"\n                >\n                  {tab.count}\n                </span>\n              ) : null}\n            </TabsTrigger>\n          ))}\n          <GlideUnderline activeId={active} />\n        </TabsList>\n        {activeTab?.actions ? (\n          <div\n            className=\"fade-in-0 flex animate-in items-center gap-1.5 pr-1 duration-200 motion-reduce:animate-none\"\n            data-slot=\"workspace-tabs-actions\"\n            key={active}\n          >\n            {activeTab.actions}\n          </div>\n        ) : null}\n      </div>\n      {tabs.map((tab) => (\n        <TabsContent\n          className=\"fade-in-0 slide-in-from-bottom-1 animate-in pt-4 duration-300 motion-reduce:animate-none\"\n          key={tab.id}\n          value={tab.id}\n        >\n          {tab.content}\n        </TabsContent>\n      ))}\n    </Tabs>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}