{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rail-stage",
  "title": "Rail Stage",
  "description": "A composed content browser that pairs a scalable vertical rail with one focused stage, collapsing to a scrollable strip on small screens.",
  "dependencies": [
    "@base-ui/react"
  ],
  "files": [
    {
      "path": "registry/base/blocks/rail-stage.tsx",
      "content": "\"use client\";\n\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type RailStageItem = {\n  /** Stable identity; also the tab's value. */\n  id: string;\n  /** Rail entry. A node rather than a string so entries can carry an icon. */\n  label: React.ReactNode;\n  /** What the stage shows while this entry is selected. */\n  content: React.ReactNode;\n  /**\n   * Optional strip pinned above the stage — a title, badges, a link out. Kept\n   * per item so it can describe whatever is currently on stage.\n   */\n  header?: React.ReactNode;\n};\n\nexport type RailStageProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"children\" | \"defaultValue\" | \"onChange\"\n> & {\n  items: RailStageItem[];\n  /** Controlled selection. Pair with `onValueChange`. */\n  value?: string;\n  /** Initial selection when uncontrolled. Defaults to the first item. */\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Which side the rail sits on once there is room for it. Defaults to \"end\". */\n  railSide?: \"start\" | \"end\";\n  /** Accessible name for the rail, which is a tab list. */\n  railLabel?: string;\n  /** Rail width once it sits beside the stage. Defaults to 220px. */\n  railWidth?: number | string;\n  /**\n   * Arrow-key axis. Defaults to \"vertical\" to match the rail's wide layout; set\n   * \"horizontal\" if you expect the collapsed strip to be the common case.\n   */\n  orientation?: \"vertical\" | \"horizontal\";\n  railClassName?: string;\n  tabClassName?: string;\n  indicatorClassName?: string;\n  stageClassName?: string;\n  headerClassName?: string;\n};\n\n/**\n * A rail of choices beside a single stage: pick an entry, the stage shows it.\n *\n * Why this exists: a gallery of live examples wants one large surface, not a grid\n * of small ones — every small tile competes for the same attention and none of\n * them reads. Tabs solve that, except a stock horizontal strip runs out of room\n * past a handful of entries. A vertical rail scales to a dozen and still reads as\n * an index of what else there is.\n *\n * Built on Base UI's Tabs, so arrow-key navigation, roving focus, and the\n * `tablist` / `tab` / `tabpanel` wiring are the primitive's job, not ours.\n *\n * @example\n *   <RailStage\n *     railLabel=\"Examples\"\n *     items={[\n *       { id: \"chart\", label: \"Chart\", header: <h3>Chart</h3>, content: <Chart /> },\n *       { id: \"table\", label: \"Table\", content: <Table /> },\n *     ]}\n *   />\n *\n * Notes:\n * - Below `sm` the rail becomes a horizontally scrollable strip above the stage,\n *   because a vertical rail plus a stage does not fit a phone. The selected entry\n *   is scrolled into view on change, so a selection made with the keyboard never\n *   lands off-screen. `orientation` stays whatever you set it to — the arrow-key\n *   axis cannot follow a media query without shipping one.\n * - The stage is `min-h-0 min-w-0` and clips, so content that animates its own\n *   size cannot stretch the shell and shift the page around it.\n * - Layout only: beyond a border and a divider the stage brings no background or\n *   backdrop of its own. Style it through `stageClassName`.\n */\nexport function RailStage({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  railSide = \"end\",\n  railLabel,\n  railWidth = 220,\n  orientation = \"vertical\",\n  className,\n  style,\n  railClassName,\n  tabClassName,\n  indicatorClassName,\n  stageClassName,\n  headerClassName,\n  ...props\n}: RailStageProps) {\n  const railRef = React.useRef<HTMLDivElement | null>(null);\n  const indicatorRef = React.useRef<HTMLSpanElement | null>(null);\n  const firstId = items[0]?.id;\n  const [uncontrolledValue, setUncontrolledValue] = React.useState(\n    () => defaultValue ?? firstId,\n  );\n  const isControlled = value !== undefined;\n  const activeId = isControlled ? value : uncontrolledValue;\n  const activeItem = items.find((item) => item.id === activeId) ?? items[0];\n\n  const handleValueChange = React.useCallback(\n    (next: unknown) => {\n      const nextId = String(next);\n\n      if (!isControlled) {\n        setUncontrolledValue(nextId);\n      }\n\n      onValueChange?.(nextId);\n    },\n    [isControlled, onValueChange],\n  );\n\n  const syncIndicator = React.useCallback(() => {\n    const rail = railRef.current;\n    const indicator = indicatorRef.current;\n    const selected = rail?.querySelector<HTMLElement>(\n      \"[data-rail-stage-active]\",\n    );\n\n    if (!rail || !indicator || !selected) return;\n\n    const indicatorHeight = Math.max(selected.offsetHeight - 16, 0);\n    const indicatorY =\n      selected.offsetTop + (selected.offsetHeight - indicatorHeight) / 2;\n\n    indicator.style.height = `${indicatorHeight}px`;\n    indicator.style.transform = `translate3d(0, ${indicatorY}px, 0)`;\n    indicator.dataset.active = \"\";\n  }, []);\n\n  React.useLayoutEffect(() => {\n    const rail = railRef.current;\n    const selected = rail?.querySelector<HTMLElement>(\n      \"[data-rail-stage-active]\",\n    );\n\n    syncIndicator();\n\n    if (!rail || !selected || typeof ResizeObserver === \"undefined\") return;\n\n    const observer = new ResizeObserver(syncIndicator);\n    observer.observe(rail);\n    observer.observe(selected);\n\n    return () => observer.disconnect();\n  }, [activeItem?.id, syncIndicator]);\n\n  // Only meaningful in the collapsed strip: keep the selected entry visible when\n  // selection moves by keyboard or from outside the component.\n  React.useEffect(() => {\n    const rail = railRef.current;\n\n    if (!rail || rail.scrollWidth <= rail.clientWidth) return;\n\n    const selected = rail.querySelector<HTMLElement>(\n      \"[data-rail-stage-active]\",\n    );\n\n    if (!selected) return;\n\n    rail.scrollTo({\n      left: Math.max(selected.offsetLeft - 16, 0),\n      behavior: \"auto\",\n    });\n  }, [activeItem?.id]);\n\n  if (!activeItem) {\n    return null;\n  }\n\n  return (\n    <TabsPrimitive.Root\n      {...props}\n      data-slot=\"rail-stage\"\n      orientation={orientation}\n      value={activeItem.id}\n      onValueChange={handleValueChange}\n      style={{\n        ...style,\n        [\"--rail-stage-rail\" as string]:\n          typeof railWidth === \"number\" ? `${railWidth}px` : railWidth,\n      }}\n      className={cn(\n        \"grid min-w-0 overflow-hidden rounded-lg border bg-card text-card-foreground\",\n        railSide === \"start\"\n          ? \"sm:grid-cols-[var(--rail-stage-rail)_minmax(0,1fr)]\"\n          : \"sm:grid-cols-[minmax(0,1fr)_var(--rail-stage-rail)]\",\n        className,\n      )}\n    >\n      <div\n        className={cn(\n          \"order-2 flex min-w-0 flex-col\",\n          railSide === \"start\" ? \"sm:order-2\" : \"sm:order-1\",\n        )}\n      >\n        {activeItem.header ? (\n          <div\n            className={cn(\n              \"min-w-0 border-b bg-muted/20 px-4 py-3\",\n              headerClassName,\n            )}\n          >\n            {activeItem.header}\n          </div>\n        ) : null}\n\n        {items.map((item) => (\n          <TabsPrimitive.Panel\n            key={item.id}\n            value={item.id}\n            className={cn(\n              \"relative flex min-h-0 min-w-0 flex-1 items-center justify-center overflow-hidden p-4 outline-none sm:p-6\",\n              stageClassName,\n            )}\n          >\n            {item.content}\n          </TabsPrimitive.Panel>\n        ))}\n      </div>\n\n      <TabsPrimitive.List\n        ref={railRef}\n        aria-label={railLabel}\n        className={cn(\n          \"relative order-1 flex min-w-0 flex-nowrap overflow-x-auto overflow-y-hidden border-b\",\n          // Hide the strip's scrollbar without depending on a project utility.\n          \"[scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n          \"sm:flex-col sm:overflow-visible sm:border-b-0\",\n          railSide === \"start\"\n            ? \"sm:order-1 sm:border-r\"\n            : \"sm:order-2 sm:border-l\",\n          railClassName,\n        )}\n      >\n        <span\n          ref={indicatorRef}\n          aria-hidden=\"true\"\n          data-slot=\"rail-stage-indicator\"\n          className={cn(\n            \"pointer-events-none absolute top-0 hidden w-px bg-foreground opacity-0 will-change-transform\",\n            \"transition-[transform,opacity] duration-200 ease-[cubic-bezier(0.645,0.045,0.355,1)]\",\n            \"data-active:opacity-100 motion-reduce:transition-none sm:block\",\n            railSide === \"start\" ? \"left-0\" : \"right-0\",\n            indicatorClassName,\n          )}\n        />\n\n        {items.map((item) => (\n          <TabsPrimitive.Tab\n            key={item.id}\n            value={item.id}\n            data-rail-stage-active={\n              item.id === activeItem.id ? \"\" : undefined\n            }\n            className={cn(\n              \"relative -mb-px flex h-11 min-w-44 flex-none items-center gap-2 border-b px-4 text-left font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground transition-colors\",\n              \"hover:bg-muted/30 hover:text-foreground\",\n              \"focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n              // The collapsed strip keeps a local underline; wide layouts use\n              // the single measured indicator above so it can slide between tabs.\n              \"data-active:bg-muted/35 data-active:text-foreground\",\n              \"data-active:after:absolute data-active:after:inset-x-4 data-active:after:bottom-0 data-active:after:h-px data-active:after:bg-foreground\",\n              \"sm:min-w-0 sm:data-active:after:hidden\",\n              tabClassName,\n            )}\n          >\n            <span className=\"min-w-0 truncate\">{item.label}</span>\n          </TabsPrimitive.Tab>\n        ))}\n      </TabsPrimitive.List>\n    </TabsPrimitive.Root>\n  );\n}\n",
      "type": "registry:block",
      "target": "components/blocks/rail-stage.tsx"
    }
  ],
  "meta": {
    "tags": [
      "tabs",
      "rail",
      "gallery",
      "roving-focus",
      "arrow-keys"
    ],
    "effects": [
      "panel-swap",
      "indicator-slide",
      "reduced-motion"
    ]
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:block"
}