{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drill-down-spines",
  "title": "Drill Down Spines",
  "description": "Drill-down navigation where every pushed level compresses into a slim clickable book spine instead of vanishing behind a back button, so the whole navigation history stands on a shelf beside you.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/drill-down-spines/component.tsx",
      "content": "\"use client\";\n\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\n\n// ---------------------------------------------------------------------------\n// SpineStack — drill-down navigation where a pushed level never vanishes\n// behind a back button: it compresses against the left edge into a slim\n// clickable book spine, so the whole navigation stack stands on a shelf\n// beside the current page, labeled and directly tappable at any depth.\n//\n// Each level's width is an explicit, always-computed pixel value (never\n// flex:auto), tracked against the container's measured width via\n// ResizeObserver. That is what lets a level's width transition smoothly in\n// EITHER direction on a single CSS `transition: width` — full page shrinking\n// to a 14px spine on push, or a 14/22px spine growing back to full width on\n// pop — both are just px-to-px interpolation, no measure-then-override FLIP\n// hack required. The transition runs on a spring-approximating overshoot\n// cubic-bezier (not a hand-rolled rAF integrator): honest about the\n// mechanism, still reads as a little bounce settling into place.\n//\n// A spine's content crossfades between a vertical Geist Mono title (the\n// resting 14px look) and a horizontal truncated one, widened to 22px, either\n// once the shelf is wide enough (a container breakpoint) or transiently on\n// hover/focus of that one spine — rotated text is genuinely harder to read,\n// so wide-enough shelves and any focused/hovered spine get the easier\n// horizontal form. A 1px inset \"lit\" edge (color-mix over --foreground, a\n// decoration, never --ns-accent) reads as the spine's physical edge catching\n// light, so the row reads as stacked thickness even at rest.\n//\n// Clicking any spine pops every level above it in one motion: the levels\n// above cascade off in reverse-push order (the most recently pushed leaves\n// first, each subsequent one staggered ~90ms behind) while the clicked\n// spine's width transitions back up to full — \"re-inflating\" in parallel\n// with, not after, the cascade above it. `prefers-reduced-motion` drops the\n// stagger and the transition entirely: pops commit their new stack\n// synchronously, entrances render at final width immediately.\n//\n// A11y: every spine is a real <button> whose accessible name is the level's\n// full title (`aria-label`; the rotated/truncated visual text is\n// `aria-hidden`, decorative only) — full horizontal names regardless of how\n// little of the title is legible on screen. All spines sit inside a `<nav>`\n// landmark labeled \"Navigation history, N levels\". Focus moves to the new\n// active page's heading (a `tabIndex={-1}` <h2>) on every push and on every\n// pop's commit, so the SPA-style navigation announces itself to assistive\n// tech the same way a full route change would. Zero dependencies, DOM+CSS\n// only — no canvas.\n// ---------------------------------------------------------------------------\n\nexport interface SpineStackLevel {\n  /** stable id — also used as the React key and as the pop target */\n  id: string;\n  /** shown as the vertical/horizontal spine label and the active page heading */\n  title: string;\n  content: ReactNode;\n}\n\nexport interface SpineStackHandle {\n  /** push a new level onto the stack; ignored if `id` already exists in the stack or a pop is mid-animation */\n  push: (level: SpineStackLevel) => void;\n  /** pop every level above `id`; no-op if `id` isn't in the stack or is already the active (top) level */\n  popTo: (id: string) => void;\n  /** pop exactly one level (convenience for a \"back\" affordance); no-op at the root */\n  pop: () => void;\n  /** replace the whole stack with a single level, instantly, no animation */\n  reset: (level: SpineStackLevel) => void;\n}\n\nexport interface SpineStackProps {\n  /** seed level for the stack; only read on mount — the stack is uncontrolled thereafter, drive it via the ref */\n  initial: SpineStackLevel;\n  /** fires after the top (active) level settles — on mount, on push, and once a pop's cascade commits */\n  onNavigate?: (id: string) => void;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nconst SPINE_NARROW = 14;\nconst SPINE_WIDE = 22;\nconst WIDE_BREAKPOINT = 640;\nconst TRANSITION_MS = 420;\nconst STAGGER_MS = 90;\nconst ENTER_MS = 380;\nconst SPRING_EASE = \"cubic-bezier(0.34, 1.56, 0.64, 1)\";\nconst MIN_ACTIVE_WIDTH = 200;\n\nfunction clamp(n: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, n));\n}\n\nexport const SpineStack = forwardRef<SpineStackHandle, SpineStackProps>(function SpineStack(\n  { initial, onNavigate, className = \"\" },\n  ref\n) {\n  const [stack, setStack] = useState<SpineStackLevel[]>(() => [initial]);\n  const [collapsingIds, setCollapsingIds] = useState<string[]>([]); // newest-first\n  const [poppingTarget, setPoppingTarget] = useState<string | null>(null);\n  const [hoveredId, setHoveredId] = useState<string | null>(null);\n  const [focusedId, setFocusedId] = useState<string | null>(null);\n  const [containerWidth, setContainerWidth] = useState(0);\n  const [reducedMotion, setReducedMotion] = useState(false);\n  const [enteringId, setEnteringId] = useState<string | null>(null);\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const headingRef = useRef<HTMLHeadingElement>(null);\n  const isAnimatingRef = useRef(false);\n  const commitTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const enterTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const isFirstRenderRef = useRef(true);\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReducedMotion(mq.matches);\n    const onChange = () => setReducedMotion(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n\n  useEffect(() => {\n    const el = containerRef.current;\n    if (!el) return;\n    setContainerWidth(el.getBoundingClientRect().width);\n    const ro = new ResizeObserver((entries) => {\n      const w = entries[0]?.contentRect.width;\n      if (w) setContainerWidth(w);\n    });\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  useEffect(\n    () => () => {\n      if (commitTimeoutRef.current) clearTimeout(commitTimeoutRef.current);\n      if (enterTimeoutRef.current) clearTimeout(enterTimeoutRef.current);\n    },\n    []\n  );\n\n  const activeId = poppingTarget ?? stack[stack.length - 1].id;\n\n  useEffect(() => {\n    if (hoveredId === activeId || (hoveredId && !stack.some((level) => level.id === hoveredId))) {\n      setHoveredId(null);\n    }\n  }, [activeId, hoveredId, stack]);\n\n  // focus + onNavigate fire once the *committed* top settles (immediately on\n  // push, or once a pop's cascade actually truncates the stack) — never\n  // mid-cascade, and never on first mount.\n  const committedActiveId = stack[stack.length - 1].id;\n  useEffect(() => {\n    if (isFirstRenderRef.current) {\n      isFirstRenderRef.current = false;\n      return;\n    }\n    if (poppingTarget) return; // still mid-cascade; effect re-fires once it clears\n    headingRef.current?.focus({ preventScroll: true });\n    onNavigate?.(committedActiveId);\n    // eslint-disable-next-line react-hooks/exhaustive-deps -- onNavigate intentionally excluded, fires on id change only\n  }, [committedActiveId, poppingTarget]);\n\n  const push = useCallback(\n    (level: SpineStackLevel) => {\n      if (isAnimatingRef.current) return;\n      setStack((prev) => {\n        if (prev.some((l) => l.id === level.id)) return prev;\n        return [...prev, level];\n      });\n      if (enterTimeoutRef.current) clearTimeout(enterTimeoutRef.current);\n      if (!reducedMotion) {\n        setEnteringId(level.id);\n        enterTimeoutRef.current = setTimeout(() => setEnteringId(null), ENTER_MS);\n      }\n    },\n    [reducedMotion]\n  );\n\n  const popTo = useCallback(\n    (id: string) => {\n      if (isAnimatingRef.current) return;\n      const idx = stack.findIndex((l) => l.id === id);\n      if (idx === -1 || idx === stack.length - 1) return;\n      const above = stack\n        .slice(idx + 1)\n        .map((l) => l.id)\n        .reverse(); // newest (most recently pushed) first\n\n      if (reducedMotion) {\n        setStack((prev) => prev.slice(0, idx + 1));\n        return;\n      }\n\n      isAnimatingRef.current = true;\n      setPoppingTarget(id);\n      setCollapsingIds(above);\n      const total = (above.length - 1) * STAGGER_MS + TRANSITION_MS + 40;\n      commitTimeoutRef.current = setTimeout(() => {\n        setStack((prev) => prev.slice(0, idx + 1));\n        setCollapsingIds([]);\n        setPoppingTarget(null);\n        isAnimatingRef.current = false;\n      }, total);\n    },\n    [stack, reducedMotion]\n  );\n\n  const pop = useCallback(() => {\n    if (stack.length < 2) return;\n    popTo(stack[stack.length - 2].id);\n  }, [stack, popTo]);\n\n  const reset = useCallback((level: SpineStackLevel) => {\n    if (commitTimeoutRef.current) clearTimeout(commitTimeoutRef.current);\n    if (enterTimeoutRef.current) clearTimeout(enterTimeoutRef.current);\n    isAnimatingRef.current = false;\n    setCollapsingIds([]);\n    setPoppingTarget(null);\n    setEnteringId(null);\n    setStack([level]);\n  }, []);\n\n  useImperativeHandle(ref, () => ({ push, popTo, pop, reset }), [push, popTo, pop, reset]);\n\n  const wide = containerWidth >= WIDE_BREAKPOINT;\n  const spineWidthFor = (id: string) => {\n    if (collapsingIds.includes(id)) return 0;\n    return wide || hoveredId === id || focusedId === id ? SPINE_WIDE : SPINE_NARROW;\n  };\n  const spinesTotal = stack.reduce((sum, l) => sum + (l.id === activeId ? 0 : spineWidthFor(l.id)), 0);\n  const GAP = 3;\n  const gapCount = Math.max(0, stack.length - 1);\n  const activeWidth = containerWidth\n    ? clamp(containerWidth - spinesTotal - gapCount * GAP, MIN_ACTIVE_WIDTH, containerWidth)\n    : undefined;\n\n  const navLabel = `Navigation history, ${stack.length} level${stack.length === 1 ? \"\" : \"s\"}`;\n\n  return (\n    <div\n      ref={containerRef}\n      className={[\"ns-drill-down-spines relative flex h-full w-full overflow-hidden\", className]\n        .filter(Boolean)\n        .join(\" \")}\n    >\n      <style>{`\n.ns-drill-down-spines .ns-level{transition:width ${TRANSITION_MS}ms ${SPRING_EASE},opacity ${TRANSITION_MS}ms ease-out;}\n.ns-drill-down-spines .ns-spine-face,\n.ns-drill-down-spines .ns-active-face{transition:opacity ${TRANSITION_MS}ms ease-out;}\n.ns-drill-down-spines .ns-spine-label{transition:opacity 200ms ease-out;}\n.ns-drill-down-spines .ns-spine-enter{animation:ns-spine-enter ${ENTER_MS}ms ${SPRING_EASE};}\n@keyframes ns-spine-enter{\n  from{opacity:0.35;transform:translateX(18px);}\n  to{opacity:1;transform:translateX(0);}\n}\n@media (prefers-reduced-motion: reduce){\n  .ns-drill-down-spines .ns-level,\n  .ns-drill-down-spines .ns-spine-face,\n  .ns-drill-down-spines .ns-active-face,\n  .ns-drill-down-spines .ns-spine-label{transition:none !important;}\n  .ns-drill-down-spines .ns-spine-enter{animation:none !important;}\n}\n`}</style>\n\n      <nav aria-label={navLabel} className=\"flex h-full w-full\" style={{ gap: GAP }}>\n        {stack.map((level) => {\n          const isActive = level.id === activeId;\n          const collapsing = !isActive && collapsingIds.includes(level.id);\n          const collapseIndex = collapsingIds.indexOf(level.id);\n          const width = isActive ? activeWidth : spineWidthFor(level.id);\n          const isWideSpine = !isActive && width === SPINE_WIDE;\n          return (\n            <div\n              key={level.id}\n              style={{\n                width,\n                minWidth: isActive ? MIN_ACTIVE_WIDTH : width,\n                opacity: collapsing ? 0 : 1,\n                transitionDelay: collapsing ? `${collapseIndex * STAGGER_MS}ms` : \"0ms\",\n              }}\n              className={[\n                \"ns-level relative h-full shrink-0 overflow-hidden rounded-md border border-border bg-surface\",\n                enteringId === level.id ? \"ns-spine-enter\" : \"\",\n              ]\n                .filter(Boolean)\n                .join(\" \")}\n            >\n              {/* spine face — the level's resting/collapsed representation. Stays mounted\n                  even while the active face is showing, so the width transition (on the\n                  shared wrapper above) and this opacity crossfade run on ONE persistent\n                  element, not a remount between two different DOM nodes. */}\n              <button\n                type=\"button\"\n                data-slot=\"spine\"\n                aria-label={level.title}\n                title={level.title}\n                inert={isActive || undefined}\n                onClick={() => popTo(level.id)}\n                onMouseEnter={() => setHoveredId(level.id)}\n                onMouseLeave={() => setHoveredId((h) => (h === level.id ? null : h))}\n                onFocus={() => setFocusedId(level.id)}\n                onBlur={() => setFocusedId((f) => (f === level.id ? null : f))}\n                style={{\n                  opacity: isActive ? 0 : 1,\n                  visibility: isActive ? \"hidden\" : \"visible\",\n                  pointerEvents: isActive ? \"none\" : undefined,\n                  boxShadow: \"inset -1px 0 0 0 color-mix(in srgb, var(--foreground) 22%, transparent)\",\n                }}\n                className=\"ns-spine-face absolute inset-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n              >\n                <span\n                  aria-hidden\n                  className=\"ns-spine-label pointer-events-none absolute inset-0 flex items-center justify-center overflow-hidden whitespace-nowrap px-1 font-mono text-[10px] tracking-wide text-ns-muted\"\n                  style={{\n                    writingMode: \"vertical-rl\",\n                    textOverflow: \"ellipsis\",\n                    opacity: isWideSpine ? 0 : 1,\n                  }}\n                >\n                  {level.title}\n                </span>\n                <span\n                  aria-hidden\n                  className=\"ns-spine-label pointer-events-none absolute inset-0 flex items-center overflow-hidden whitespace-nowrap px-1.5 font-mono text-[10px] tracking-wide text-ellipsis text-ns-muted\"\n                  style={{ opacity: isWideSpine ? 1 : 0 }}\n                >\n                  {level.title}\n                </span>\n              </button>\n\n              {/* active face — the level's full page. Mounted for every level, always,\n                  so a spine popping back to active re-inflates in place instead of\n                  swapping to a freshly-mounted pane. */}\n              <div\n                inert={isActive ? undefined : true}\n                style={{\n                  opacity: isActive ? 1 : 0,\n                  visibility: isActive ? \"visible\" : \"hidden\",\n                  pointerEvents: isActive ? undefined : \"none\",\n                }}\n                className=\"ns-active-face absolute inset-0 overflow-auto bg-background\"\n              >\n                <div className=\"flex h-full flex-col p-4\">\n                  <h2\n                    ref={isActive ? headingRef : undefined}\n                    tabIndex={-1}\n                    className=\"mb-3 text-base font-semibold tracking-tight text-foreground outline-none\"\n                  >\n                    {level.title}\n                  </h2>\n                  <div className=\"min-h-0 flex-1\">{level.content}</div>\n                </div>\n              </div>\n            </div>\n          );\n        })}\n      </nav>\n    </div>\n  );\n});\n",
      "type": "registry:ui",
      "target": "components/ui/drill-down-spines.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "navigation",
      "drill-down",
      "master-detail",
      "breadcrumb",
      "nav",
      "stack",
      "history"
    ],
    "instruction": "Build a drill-down navigation primitive for master-detail and multi-level record hierarchies (folders, nested tickets, org charts) where pushing a new level does not hide the previous one behind a back button: the outgoing page compresses in place into a slim clickable spine that stays permanently visible on a shelf to the left of whatever is currently active, at any depth. Manage the stack uncontrolled, driven imperatively via a ref handle: `push({ id, title, content })` appends a new active level (no-op if the id already exists in the stack, or if a pop is mid-animation), `popTo(id)` collapses every level above `id` back to it, `pop()` is a one-level-back convenience, `reset(level)` replaces the whole stack instantly with no animation. Critically, every level in the stack is ONE persistent wrapper element for its entire lifetime — mounted once when pushed, unmounted only when actually popped past — never two different elements swapped between an 'active' tree position and a 'spine' tree position. Inside that wrapper are two always-mounted, absolutely-positioned faces: a spine-face `<button>` (vertical/horizontal title, click-to-popTo) and an active-face `<div>` (heading + content), cross-fading via opacity, with `inert` applied to whichever face is currently not the front one so its buttons/inputs are unreachable and hidden from the accessibility tree without unmounting it. The active-face additionally gets `pointer-events: none` while inert (not just `inert` alone) — `inert` removes an element from the accessibility tree but `document.elementFromPoint`/real hit-testing still resolves to whatever is topmost in paint order, so without an explicit `pointer-events: none` the invisible (opacity 0) active-face sitting on top would swallow clicks meant for the spine-face button beneath it. Because the wrapper never remounts, the wrapper's own width — an explicit pixel value every render, computed from a ResizeObserver-measured container width minus the sum of every other level's current spine width (14px resting, 22px widened) and inter-item gaps, never `flex:auto` — transitions cleanly on a single `transition: width` in EITHER direction on that same node: springing down from full to 14px as a level is pushed past, or re-inflating from 14/22px back to full as a popTo target becomes active again. The timing function is a spring-approximating overshoot cubic-bezier (`cubic-bezier(0.34, 1.56, 0.64, 1)`), not a hand-rolled physics integrator — an honest choice for a DOM/CSS-only nav primitive, and it still reads as a small bounce settling into place. Pushing a level: the previously-active wrapper's width springs down to spine width while its active-face fades out and its spine-face fades in — a horizontal heading crossfading to a `writing-mode: vertical-rl` Geist Mono title in `--ns-muted` (two absolutely-positioned spans within the spine-face, both `aria-hidden` since the button's real accessible name is a plain aria-label carrying the full title); the freshly pushed level is a brand-new wrapper, mounted already at its resolved active width, playing a short slide-and-fade-in keyframe from the right (`translateX(18px)` -> `translateX(0)`) since there is no prior state for a new element to interpolate from. Each spine-face carries a 1px inset right edge via `box-shadow: inset -1px 0 0 0 color-mix(in srgb, var(--foreground) 22%, transparent)` — a decorative lit edge (never `--ns-accent`, which appears only on focus rings) so the row reads as physical stacked thickness even at rest. Clicking any spine calls `popTo` on it: every level above cascades off in reverse-push order on its own still-mounted wrapper (the most recently pushed level's width and opacity go to 0 first, each earlier one staggered 90ms further behind via `transition-delay`) while the clicked level's own wrapper simultaneously transitions its width back up to full and crossfades its active-face back in — the re-inflation runs in parallel with the cascade above it, not after, because it is the same element animating, not a fresh mount. A11y: spine-faces are real `<button>`s, all levels sit inside one `<nav aria-label=\"Navigation history, N levels\">` landmark (N = total stack depth including the active page — the active page's content living inside the same landmark as the history is an accepted minor semantic tradeoff for keeping every level a single animatable element); the rotated/truncated visual label is decorative only, the accessible name is always the full title via `aria-label` plus a native `title` tooltip. Above a ~640px container-width breakpoint every spine permanently widens to 22px with a horizontal truncated label instead of the rotated 14px one (rotated text is genuinely harder to read); below that breakpoint, hovering or focusing one individual spine widens just that spine to 22px with the same horizontal label, reverting once hover/focus leaves. Focus moves to the newly active page's heading (a `tabIndex={-1}` `<h2>`, one per level, each inert while its level isn't active) on every push and once every pop's cascade actually commits (never mid-cascade) — the same SPA-route-change focus pattern a full page navigation would use, and `onNavigate(id)` fires at that same moment. `prefers-reduced-motion` drops every transition, the stagger, and the entrance keyframe: pops commit their new stack synchronously and every level renders at its resolved final width immediately, fully usable and legible, just not eased into. Zero dependencies, DOM+CSS only — no canvas. Differs from toast-gravity-stack, which accumulates dropped toast data as horizontal strata piling under gravity (a core sample of events that arrived, read once and dismissed): drill-down-spines accumulates navigation depth as vertical spines that are each a live, permanently clickable route target back into the hierarchy — a bookshelf you can reach into at any depth, not a core sample you only ever read from the top."
  },
  "type": "registry:ui"
}