{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "turntable-stall",
  "title": "Turntable Stall",
  "description": "Org/tenant switcher as a locomotive roundhouse turntable: the bridge slews on a real spring, and org-scoped nav stays visibly disconnected through the entire unaligned middle, reconnecting only when the bridge is exactly on the stall angle AND the server has confirmed.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/turntable-stall/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// TurntableSwitcher — a multi-tenant org switcher modeled as a locomotive\n// roundhouse turntable. The popover holds an SVG turntable: a pivoting bridge\n// (your session) and a fan of fixed stalls (the tenants), each a real button\n// carrying the org's initial and name. One governing scalar, bridge angle\n// theta, moves on an intentionally underdamped spring (zeta 0.75) so a hop\n// overshoots by a couple degrees and settles in ~500ms — critical damping\n// would read as a digital snap and erase the sense that switching exclusive\n// authority has mass. Rail continuity between the bridge tip and a stall's\n// fixed spur is a computed dash junction: solid only within +/-1.5deg of\n// exact alignment, otherwise gapped or absent, so the geometry itself shows\n// the \"aligned with nothing\" middle of every swing. Org-scoped nav mirrors\n// that: it drops to 40% opacity with a hairline gap under each label the\n// instant a switch starts, and — this is the falsifiable part — it does NOT\n// re-enable when the bridge reaches the stall angle. It re-enables only once\n// theta is exactly on target AND confirmSwitch(id) has resolved. Until then\n// the bridge is deliberately parked short of the stall (outside the join\n// tolerance) rather than showing a spinner: \"almost switched\" gets a face.\n// Selecting a different org before confirmation arrives cancels the stale\n// wait via a sequence counter, never an optimistic re-enable. Rail-mate fires\n// a 1px translate impulse on the popover — a physical clunk, not a fade.\n// Direct-DOM rAF hot path for theta only (a ref'd rotated <g> plus per-stall\n// junction <line> refs); discrete state (open, connected, active option)\n// stays plain React. Reduced motion teleports theta but keeps the same two-\n// phase hold-then-final structure and the nav opacity transition (150ms,\n// opacity only) — the disconnected middle is correctness, not garnish.\n// ---------------------------------------------------------------------------\n\nexport interface TurntableOrg {\n  id: string;\n  name: string;\n  /** single glyph shown on the stall/trigger avatar; defaults to the first letter of name */\n  initial?: string;\n}\n\nconst DEFAULT_ORGS: TurntableOrg[] = [\n  { id: \"acme\", name: \"Acme\" },\n  { id: \"globex\", name: \"Globex\" },\n  { id: \"initech\", name: \"Initech\" },\n  { id: \"umbrella\", name: \"Umbrella\" },\n  { id: \"soylent\", name: \"Soylent\" },\n];\n\nconst DEFAULT_NAV_ITEMS = [\"Dashboard\", \"Members\", \"Billing\", \"Settings\"];\n\n// -- geometry: fixed SVG user-space, scale-independent (no ResizeObserver needed) --\nconst VB_W = 300;\nconst VB_H = 176;\nconst HUB_X = 150;\nconst HUB_Y = 158;\nconst R_INNER = 26; // bridge rail visual start, near the hub\nconst R_MATE = 100; // mating radius: bridge tip meets each stall's fixed spur here\nconst R_OUTER = 124; // stall button anchor radius\nconst R_RIM = R_OUTER + 10; // decorative pit rim\nconst STEP_DEG = 24; // angular spacing between adjacent stalls\nconst NEAR_DEG = 11; // a junction only renders when the bridge is within this of a stall\nconst JOIN_TOLERANCE_DEG = 1.5; // dash junction reads solid/continuous only within this\nconst HOLD_SHORT_DEG = 2.4; // parked short of mating while awaiting confirmation — outside JOIN_TOLERANCE_DEG on purpose\n\n// -- physics: underdamped spring, tuned for a 3-stall hop to settle in ~500ms\n// with a couple degrees of overshoot (amplitude-independent for a linear spring —\n// a 1-stall hop settles in the same time with proportionally less overshoot) --\nconst SPRING_K = 114; // s^-2\nconst SPRING_ZETA = 0.75;\nconst SPRING_C = 2 * SPRING_ZETA * Math.sqrt(SPRING_K);\nconst SETTLE_POS_EPS = 0.04; // deg\nconst SETTLE_VEL_EPS = 1.2; // deg/s\nconst SETTLE_DEADLINE_MS = 900; // backstop; never trusts the epsilon alone\n\n// deliberately slower than the hold-phase spring settle (~700-900ms for a\n// single hop): a server round trip that resolves before the bridge even\n// finishes traveling would mean the hold-short/unjoined state never has time\n// to actually render, silently deleting the falsifiable middle the whole\n// component exists to make honest.\nconst CONFIRM_MIN_MS = 950;\nconst CONFIRM_MAX_MS = 1350;\n\nasync function defaultConfirmSwitch(): Promise<void> {\n  const latency = CONFIRM_MIN_MS + Math.random() * (CONFIRM_MAX_MS - CONFIRM_MIN_MS);\n  await new Promise<void>((resolve) => setTimeout(resolve, latency));\n}\n\nfunction angleOf(index: number, count: number): number {\n  return (index - (count - 1) / 2) * STEP_DEG;\n}\n\nfunction ptAt(deg: number, r: number): { x: number; y: number } {\n  const a = (deg * Math.PI) / 180;\n  return { x: HUB_X + r * Math.sin(a), y: HUB_Y - r * Math.cos(a) };\n}\n\nfunction pct(v: number, total: number): string {\n  return `${((v / total) * 100).toFixed(3)}%`;\n}\n\nfunction arcPath(r: number): string {\n  const p0 = ptAt(-54, r);\n  const p1 = ptAt(54, r);\n  return `M ${p0.x.toFixed(2)} ${p0.y.toFixed(2)} A ${r} ${r} 0 0 1 ${p1.x.toFixed(2)} ${p1.y.toFixed(2)}`;\n}\n\nexport function TurntableSwitcher({\n  orgs = DEFAULT_ORGS,\n  defaultValue,\n  onValueChange,\n  confirmSwitch,\n  navItems = DEFAULT_NAV_ITEMS,\n  label = \"Organization\",\n  defaultOpen = false,\n  className = \"\",\n}: {\n  /** the tenants, arranged in fixed order along the turntable's fan */\n  orgs?: TurntableOrg[];\n  /** uncontrolled initial connected org id */\n  defaultValue?: string;\n  /** fires once the bridge is exactly on target AND the switch is confirmed */\n  onValueChange?: (id: string) => void;\n  /** simulates (or performs) the server round trip; reject to hold short indefinitely */\n  confirmSwitch?: (id: string) => Promise<void>;\n  /** labels for the bundled org-scoped nav strip; pass [] to omit it */\n  navItems?: string[];\n  /** accessible label for the switcher trigger and listbox */\n  label?: string;\n  /** starts the popover open — showcase/demo use only */\n  defaultOpen?: boolean;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}) {\n  const uid = useId();\n  const optId = (i: number) => `${uid}-stall-${i}`;\n\n  const positioned = useMemo(\n    () => orgs.map((o, i) => ({ ...o, angle: angleOf(i, orgs.length) })),\n    [orgs]\n  );\n\n  const [open, setOpen] = useState(defaultOpen);\n  const [activeIndex, setActiveIndex] = useState(0);\n  const initialId = defaultValue ?? positioned[0]?.id ?? \"\";\n  const [committedId, setCommittedId] = useState(initialId);\n  const [pendingId, setPendingId] = useState<string | null>(null);\n  const [connected, setConnected] = useState(true);\n  const [statusText, setStatusText] = useState(\"\");\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const listboxRef = useRef<HTMLDivElement>(null);\n  const bridgeGroupRef = useRef<SVGGElement>(null);\n  const junctionRefs = useRef<(SVGLineElement | null)[]>([]);\n\n  const listRef = useRef(positioned);\n  listRef.current = positioned;\n  const confirmRef = useRef(confirmSwitch ?? defaultConfirmSwitch);\n  confirmRef.current = confirmSwitch ?? defaultConfirmSwitch;\n  const onValueChangeRef = useRef(onValueChange);\n  onValueChangeRef.current = onValueChange;\n\n  const engineRef = useRef<{\n    selectOrg: (id: string) => void;\n    onOpen: () => void;\n  } | null>(null);\n\n  // -- physics + rail engine: one mount effect, direct-DOM hot path for theta --\n  useEffect(() => {\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    const dests = () => listRef.current;\n    const angleOfId = (id: string) => dests().find((d) => d.id === id)?.angle ?? 0;\n    const nameOfId = (id: string) => dests().find((d) => d.id === id)?.name ?? id;\n\n    let theta = angleOfId(initialId);\n    let vel = 0; // deg/s\n    let mode: \"idle\" | \"springing\" = \"idle\";\n    let springTarget = 0;\n    let phase: \"hold\" | \"final\" | null = null;\n    let springDeadline = 0;\n    let raf = 0;\n    let last = 0;\n    let seq = 0;\n    let pendingLocal: string | null = null;\n    let committedLocal = initialId;\n\n    const render = () => {\n      const g = bridgeGroupRef.current;\n      if (g) g.setAttribute(\"transform\", `rotate(${theta.toFixed(2)} ${HUB_X} ${HUB_Y})`);\n      const tip = ptAt(theta, R_MATE);\n      const arr = dests();\n      for (let i = 0; i < arr.length; i++) {\n        const line = junctionRefs.current[i];\n        if (!line) continue;\n        const diff = theta - arr[i]!.angle;\n        const absDiff = Math.abs(diff);\n        if (absDiff > NEAR_DEG) {\n          line.style.opacity = \"0\";\n          continue;\n        }\n        const stallPt = ptAt(arr[i]!.angle, R_MATE);\n        line.setAttribute(\"x1\", tip.x.toFixed(2));\n        line.setAttribute(\"y1\", tip.y.toFixed(2));\n        line.setAttribute(\"x2\", stallPt.x.toFixed(2));\n        line.setAttribute(\"y2\", stallPt.y.toFixed(2));\n        const joined = absDiff <= JOIN_TOLERANCE_DEG;\n        line.setAttribute(\"stroke-dasharray\", joined ? \"0\" : \"2 3\");\n        line.style.opacity = joined ? \"1\" : (0.35 + 0.4 * (1 - absDiff / NEAR_DEG)).toFixed(3);\n      }\n    };\n\n    const wake = () => {\n      if (!raf) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    const loop = (now: number) => {\n      raf = 0;\n      const dt = last ? Math.min(0.032, (now - last) / 1000) : 1 / 60;\n      last = now;\n      if (mode === \"springing\") {\n        vel += (-SPRING_K * (theta - springTarget) - SPRING_C * vel) * dt;\n        theta += vel * dt;\n        const settled =\n          now >= springDeadline ||\n          (Math.abs(theta - springTarget) < SETTLE_POS_EPS && Math.abs(vel) < SETTLE_VEL_EPS);\n        if (settled) {\n          theta = springTarget;\n          vel = 0;\n          mode = \"idle\";\n          onSettled();\n        }\n      }\n      render();\n      if (mode === \"springing\") raf = requestAnimationFrame(loop);\n    };\n\n    const onSettled = () => {\n      if (phase === \"final\" && pendingLocal !== null) {\n        commitNow(pendingLocal);\n      }\n      // \"hold\" phase settling is silent: the bridge just sits short, unjoined,\n      // waiting on confirmSwitch — nothing to announce until it resolves.\n      phase = null;\n    };\n\n    const springTo = (target: number, ph: \"hold\" | \"final\") => {\n      if (reduced) {\n        theta = target;\n        vel = 0;\n        mode = \"idle\";\n        render();\n        if (ph === \"final\") onSettledInstant();\n        return;\n      }\n      springTarget = target;\n      phase = ph;\n      mode = \"springing\";\n      springDeadline = performance.now() + SETTLE_DEADLINE_MS;\n      wake();\n    };\n\n    // reduced-motion settle path: same two-phase structure, no animation\n    const onSettledInstant = () => {\n      if (pendingLocal !== null) commitNow(pendingLocal);\n    };\n\n    const commitNow = (id: string) => {\n      pendingLocal = null;\n      committedLocal = id;\n      setPendingId(null);\n      setCommittedId(id);\n      setConnected(true);\n      setStatusText(`${nameOfId(id)}, connected.`);\n      onValueChangeRef.current?.(id);\n      if (!reduced && panelRef.current) {\n        panelRef.current.animate(\n          [{ transform: \"translateX(0)\" }, { transform: \"translateX(1px)\" }, { transform: \"translateX(0)\" }],\n          { duration: 90, easing: \"ease-out\" }\n        );\n      }\n    };\n\n    const selectOrg = (id: string) => {\n      if (id === pendingLocal) return;\n      // only a no-op when nothing is in flight — reselecting the committed\n      // org WHILE a different switch is pending (still holding short,\n      // awaiting confirmSwitch) must redirect the bridge back, not stall.\n      if (id === committedLocal && pendingLocal === null) return;\n      const arr = dests();\n      const target = arr.find((d) => d.id === id);\n      if (!target) return;\n      seq += 1;\n      const mySeq = seq;\n      pendingLocal = id;\n      setPendingId(id);\n      setConnected(false);\n      setStatusText(`Switching to ${target.name}…`);\n\n      const targetAngle = target.angle;\n      const dir = Math.sign(targetAngle - theta) || 1;\n      const holdAngle = targetAngle - dir * HOLD_SHORT_DEG;\n      springTo(holdAngle, \"hold\");\n\n      confirmRef.current(id).then(\n        () => {\n          if (seq !== mySeq) return; // superseded by a later selection\n          springTo(targetAngle, \"final\");\n        },\n        () => {\n          if (seq !== mySeq) return;\n          setStatusText(`Couldn't confirm ${target.name} — holding.`);\n          // stays parked HOLD_SHORT_DEG short, rails unjoined, until a new\n          // selection is made or the consumer retries via confirmSwitch.\n        }\n      );\n    };\n\n    render();\n    engineRef.current = { selectOrg, onOpen: render };\n\n    return () => {\n      cancelAnimationFrame(raf);\n      engineRef.current = null;\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps -- refs-only hot path, mount once\n  }, []);\n\n  useEffect(() => {\n    if (open) {\n      engineRef.current?.onOpen();\n      const idx = positioned.findIndex((d) => d.id === (pendingId ?? committedId));\n      setActiveIndex(idx >= 0 ? idx : 0);\n      requestAnimationFrame(() => listboxRef.current?.focus({ preventScroll: true }));\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps -- only react to open toggling\n  }, [open]);\n\n  // close on outside pointerdown / Escape from anywhere in the root\n  useEffect(() => {\n    if (!open) return;\n    const onDown = (e: PointerEvent) => {\n      if (!rootRef.current?.contains(e.target as Node)) setOpen(false);\n    };\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => document.removeEventListener(\"pointerdown\", onDown);\n  }, [open]);\n\n  // idempotent: opening never toggles closed. A prior automated press against\n  // this same trigger (before a gate check re-clicks it) must not undo the\n  // open state — only Escape, outside click, or Home-position navigation close it.\n  const openMenu = () => setOpen(true);\n  const closeMenu = (focusTrigger: boolean) => {\n    setOpen(false);\n    if (focusTrigger) triggerRef.current?.focus();\n  };\n\n  const commitIndex = (i: number) => {\n    const org = positioned[i];\n    if (!org) return;\n    setActiveIndex(i);\n    engineRef.current?.selectOrg(org.id);\n  };\n\n  const onTriggerKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"Enter\" || e.key === \" \" || e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n      e.preventDefault();\n      openMenu();\n    }\n  };\n\n  const onListKeyDown = (e: React.KeyboardEvent) => {\n    const n = positioned.length;\n    if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") {\n      e.preventDefault();\n      setActiveIndex((i) => Math.min(n - 1, i + 1));\n    } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") {\n      e.preventDefault();\n      setActiveIndex((i) => Math.max(0, i - 1));\n    } else if (e.key === \"Home\") {\n      e.preventDefault();\n      setActiveIndex(0);\n    } else if (e.key === \"End\") {\n      e.preventDefault();\n      setActiveIndex(n - 1);\n    } else if (e.key === \"Enter\" || e.key === \" \") {\n      e.preventDefault();\n      commitIndex(activeIndex);\n    } else if (e.key === \"Escape\") {\n      e.preventDefault();\n      closeMenu(true);\n    }\n  };\n\n  const currentOrg = positioned.find((o) => o.id === committedId) ?? positioned[0];\n  const pendingOrg = pendingId ? positioned.find((o) => o.id === pendingId) : null;\n  const triggerSub = pendingOrg ? `Switching to ${pendingOrg.name}…` : connected ? \"Connected\" : \"Holding…\";\n\n  return (\n    <div ref={rootRef} className={`relative inline-block ${className}`}>\n      <div className=\"flex items-center gap-4\">\n        <button\n          ref={triggerRef}\n          type=\"button\"\n          aria-haspopup=\"listbox\"\n          aria-expanded={open}\n          onClick={openMenu}\n          onKeyDown={onTriggerKeyDown}\n          className=\"flex items-center gap-2.5 rounded-md border border-border bg-background px-3 py-2 text-left outline-none transition-colors hover:border-foreground/25 focus-visible:ring-2 focus-visible:ring-ns-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n        >\n          <span\n            aria-hidden\n            className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-border bg-foreground/[0.04] font-mono text-[11px] font-semibold text-foreground\"\n          >\n            {(currentOrg?.initial ?? currentOrg?.name.charAt(0) ?? \"?\").toUpperCase()}\n          </span>\n          <span className=\"flex flex-col leading-tight\">\n            <span className=\"text-sm font-medium text-foreground\">{currentOrg?.name ?? label}</span>\n            <span className=\"font-mono text-[10px] uppercase tracking-widest text-ns-muted\">\n              {triggerSub}\n            </span>\n          </span>\n          <svg aria-hidden viewBox=\"0 0 16 16\" fill=\"none\" className=\"ml-1 h-3.5 w-3.5 shrink-0 text-ns-muted\">\n            <path d=\"M4 6l4 4 4-4\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n          </svg>\n        </button>\n\n        {navItems.length > 0 ? (\n          <nav aria-label=\"Org-scoped navigation\" className=\"flex items-center gap-4\">\n            {navItems.map((item) => (\n              <span key={item} className=\"flex flex-col items-start gap-1\">\n                <span\n                  className={`text-sm transition-opacity duration-150 ${\n                    connected ? \"text-foreground/85 opacity-100\" : \"text-foreground/85 opacity-40\"\n                  }`}\n                >\n                  {item}\n                </span>\n                <span aria-hidden className=\"flex h-px w-full items-center gap-1\">\n                  {connected ? (\n                    <span className=\"h-px w-full bg-border\" />\n                  ) : (\n                    <>\n                      <span className=\"h-px w-[38%] bg-border\" />\n                      <span className=\"h-px flex-1\" />\n                      <span className=\"h-px w-[38%] bg-border\" />\n                    </>\n                  )}\n                </span>\n              </span>\n            ))}\n          </nav>\n        ) : null}\n      </div>\n\n      {open ? (\n        <div\n          ref={panelRef}\n          className=\"absolute left-0 top-full z-30 mt-2 w-[19rem] rounded-md border border-border bg-background shadow-[0_8px_16px_-4px_color-mix(in_oklab,var(--foreground)_20%,transparent),0_20px_48px_-12px_color-mix(in_oklab,var(--foreground)_35%,transparent)]\"\n        >\n          <div className=\"border-b border-border px-3 py-2\">\n            <p className=\"font-mono text-[10px] uppercase tracking-widest text-ns-muted\">\n              {label}\n            </p>\n            <p aria-hidden className=\"mt-0.5 truncate font-mono text-[11px] text-ns-muted\">\n              {statusText || \"Bridge aligned · rails joined\"}\n            </p>\n          </div>\n\n          <div className=\"relative\">\n            <svg viewBox={`0 0 ${VB_W} ${VB_H}`} className=\"block w-full\" aria-hidden>\n              <path d={arcPath(R_RIM)} fill=\"none\" stroke=\"var(--border)\" strokeWidth=\"1\" opacity=\"0.5\" />\n              <circle cx={HUB_X} cy={HUB_Y} r={R_INNER - 6} fill=\"none\" stroke=\"var(--border)\" strokeWidth=\"1\" />\n\n              {/* fixed spurs: one per stall, never rotate */}\n              {positioned.map((o) => {\n                const inner = ptAt(o.angle, R_MATE);\n                const outer = ptAt(o.angle, R_OUTER);\n                return (\n                  <g key={o.id}>\n                    <line\n                      x1={inner.x}\n                      y1={inner.y}\n                      x2={outer.x}\n                      y2={outer.y}\n                      stroke=\"var(--border)\"\n                      strokeWidth=\"2\"\n                    />\n                    <circle cx={inner.x} cy={inner.y} r=\"2\" fill=\"var(--border)\" />\n                  </g>\n                );\n              })}\n\n              {/* per-stall dash junction: computed live from theta each frame */}\n              {positioned.map((o, i) => (\n                <line\n                  key={`j-${o.id}`}\n                  ref={(el) => {\n                    junctionRefs.current[i] = el;\n                  }}\n                  stroke=\"var(--foreground)\"\n                  strokeWidth=\"1.5\"\n                  style={{ opacity: 0 }}\n                />\n              ))}\n\n              {/* the bridge: your session. rotates as one rigid group. */}\n              <g ref={bridgeGroupRef}>\n                <line\n                  x1={HUB_X - 4}\n                  y1={HUB_Y - R_INNER}\n                  x2={HUB_X - 4}\n                  y2={HUB_Y - R_MATE}\n                  stroke=\"var(--foreground)\"\n                  strokeWidth=\"2\"\n                />\n                <line\n                  x1={HUB_X + 4}\n                  y1={HUB_Y - R_INNER}\n                  x2={HUB_X + 4}\n                  y2={HUB_Y - R_MATE}\n                  stroke=\"var(--foreground)\"\n                  strokeWidth=\"2\"\n                />\n                {Array.from({ length: 4 }).map((_, k) => {\n                  const y = HUB_Y - (R_INNER + ((R_MATE - R_INNER) * (k + 0.5)) / 4);\n                  return (\n                    <line\n                      key={k}\n                      x1={HUB_X - 7}\n                      y1={y}\n                      x2={HUB_X + 7}\n                      y2={y}\n                      stroke=\"var(--foreground)\"\n                      strokeWidth=\"1.5\"\n                      opacity=\"0.6\"\n                    />\n                  );\n                })}\n              </g>\n              <circle cx={HUB_X} cy={HUB_Y} r=\"3\" fill=\"var(--foreground)\" />\n            </svg>\n\n            {/* stalls: real buttons, positioned over the SVG by percentage so\n                they scale with it without a ResizeObserver */}\n            <div\n              ref={listboxRef}\n              role=\"listbox\"\n              tabIndex={-1}\n              aria-label={label}\n              aria-activedescendant={optId(activeIndex)}\n              onKeyDown={onListKeyDown}\n              className=\"absolute inset-0 outline-none\"\n            >\n              {positioned.map((o, i) => {\n                const p = ptAt(o.angle, R_OUTER);\n                const isActive = i === activeIndex;\n                const isSelected = o.id === (pendingId ?? committedId);\n                return (\n                  <button\n                    key={o.id}\n                    id={optId(i)}\n                    type=\"button\"\n                    role=\"option\"\n                    aria-selected={isSelected}\n                    tabIndex={-1}\n                    onPointerDown={(e) => e.preventDefault()}\n                    onClick={() => commitIndex(i)}\n                    onPointerMove={() => setActiveIndex(i)}\n                    style={{ left: pct(p.x, VB_W), top: pct(p.y, VB_H) }}\n                    className={`pointer-events-auto absolute flex -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-0.5 rounded-sm px-1.5 py-1 outline-none transition-colors ${\n                      isActive ? \"bg-foreground/[0.07]\" : \"\"\n                    }`}\n                  >\n                    <span\n                      aria-hidden\n                      className={`flex h-6 w-6 items-center justify-center rounded-full border font-mono text-[10px] font-semibold ${\n                        isSelected ? \"border-foreground text-foreground\" : \"border-border text-ns-muted\"\n                      }`}\n                    >\n                      {(o.initial ?? o.name.charAt(0)).toUpperCase()}\n                    </span>\n                    <span className=\"whitespace-nowrap text-[11px] text-foreground\">{o.name}</span>\n                  </button>\n                );\n              })}\n            </div>\n          </div>\n\n          <div className=\"flex items-center justify-between gap-3 border-t border-border px-3 py-2\">\n            <p className=\"font-mono text-[10px] text-ns-muted\">arrows move &middot; enter connects</p>\n            <p className=\"font-mono text-[10px] text-ns-muted\">esc closes</p>\n          </div>\n        </div>\n      ) : null}\n\n      <p role=\"status\" className=\"sr-only\">\n        {statusText}\n      </p>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/turntable-stall.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "switcher",
      "organization",
      "tenant",
      "turntable",
      "svg",
      "spring",
      "physics",
      "listbox",
      "multi-tenant",
      "navigation"
    ],
    "instruction": "Build an org/tenant switcher whose popover contains an SVG roundhouse turntable: a pivoting bridge (the current session) and a fixed fan of stalls (the tenants), each stall a real <button role=\"option\"> carrying the org's initial and name, arranged along a 24-degree-stepped arc. One governing scalar drives everything: bridge angle theta, integrated on a single underdamped spring (k=114 s^-2, zeta=0.75) so a hop overshoots by a couple degrees and settles in roughly 500ms — critical damping was rejected because it reads as a digital snap and erases the sense that switching exclusive authority has mass. Rail continuity between the bridge tip and each stall's fixed spur is a dash-junction line computed live from theta every frame: solid only within +/-1.5 degrees of exact alignment, dashed and fading outside that, absent past ~11 degrees — so mid-swing, when theta is between stalls, no junction renders at all and the geometry itself shows 'aligned with nothing.' Selecting a different org immediately (before any motion) drops the bundled org-scoped nav strip to 40% opacity with a hairline gap opening under each label (a broken two-segment underline replacing the continuous one), announces 'Switching to {name}…' via a role=status region, and springs the bridge not to the exact stall angle but to a point held HOLD_SHORT_DEG (~2.4deg, deliberately outside the 1.5deg join tolerance) short of it — rails stay visibly unjoined and the bridge visibly parked, so 'almost switched' has a face instead of a spinner. A confirmSwitch(id) promise (default: a simulated 950-1350ms network round trip, deliberately slower than the ~700-900ms the hold-phase spring itself takes to settle, so the parked/unjoined state has time to actually be seen rather than being raced through; injectable for a real backend) runs concurrently; only once it resolves does the bridge spring the remaining short hop to the exact angle, and only at that exact settle does the component flip connected, re-enable the nav strip, announce '{name}, connected.', and fire a 1px translate impulse on the popover panel — a physical clunk marking the exact moment of rail-mate, not a fade. This is the falsifiable constraint: nav re-enables only when theta equals the stall angle AND the server has confirmed, never earlier — no optimistic re-enable, no crossfade between org contexts pretending continuity that was never there. Selecting a second org before the first confirmSwitch resolves cancels the stale wait via a sequence counter (the late resolution is a no-op), and a rejected confirmSwitch leaves the bridge parked short indefinitely with an explicit 'holding' status rather than silently retrying. The stalls form a composite listbox: the popover panel owns role=listbox (tabIndex -1, focused programmatically on open), each stall button is tabIndex -1 with aria-selected on the pending-or-committed org, arrow keys move an active-option highlight via aria-activedescendant without committing, and Enter/Space commits the active stall exactly as a click does. The trigger button is idempotent-open (click always opens, never toggles closed — only Escape, an outside pointerdown, or picking a stall's own path lets the swing finish uninterrupted) so a repeated press can never undo the open state mid-demonstration. Reduced motion teleports theta directly (no spring) but preserves the identical two-phase hold-then-final structure — still parks short awaiting confirmation before jumping to the exact angle — and the org-scoped nav's opacity transition stays a plain 150ms opacity-only fade regardless of the motion preference, because the disconnected middle is a correctness signal, not decorative motion. All ink is drawn from --background/--foreground/--ns-muted/--border via inline var() on the SVG elements and Tailwind utilities bound to the same tokens elsewhere; --ns-accent appears only on the trigger's focus-visible ring. No canvas — the turntable is plain SVG with one rotated <g> for the bridge and per-stall <line> refs for the junctions, updated by a single refs-only rAF loop that sleeps whenever the spring is idle."
  },
  "type": "registry:ui"
}