{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "idler-drop",
  "title": "Idler Drop",
  "description": "Delegated admin drawn as a gear train: your authority is the drive gear, each delegate is a driven gear whose tooth count sets a discrete role ratio, and revoking swings the idler out so their gear freewheels to a stop while yours keeps turning.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/idler-drop/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// IdlerDrop — delegated admin drawn as a gear train. Your authority is a\n// 24-tooth drive gear that turns continuously (one shared phase, 0.15 rev/s,\n// applied identically to every row so meshes can never drift). Each delegate\n// is a driven gear coupled through a swinging idler; its tooth count is fixed\n// by role (admin 12, editor 8, viewer 3 — a discrete authority ratio, not a\n// continuous radius) so the mesh, not a stored value, is what grants them\n// anything. Revoke swings the idler carrier out over 250ms — the driven gear\n// leaves the shared phase and freewheels down its own exponential (tau\n// ~1.2s) while the drive gear keeps turning. That coast IS the disclosure:\n// the delegate's existing session outlives the grant until it expires.\n// Rotation is refs-only, imperative `setAttribute(\"transform\", …)` on a\n// single rAF loop; React state only touches the rare, discrete stuff (role,\n// revoked, the coasting readout, the live region).\n// ---------------------------------------------------------------------------\n\nexport type DelegateRole = \"admin\" | \"editor\" | \"viewer\";\n\nexport type Delegation = {\n  id: string;\n  name: string;\n  role: DelegateRole;\n  /** epoch ms the grant was made */\n  grantedAt: number;\n  /** epoch ms the delegate's underlying session token actually expires —\n   *  real data the post-revoke \"coasting\" readout is computed from, not a\n   *  scripted countdown. */\n  sessionExpiresAt: number;\n};\n\ntype Row = Delegation & {\n  revoked: boolean;\n  coastingMinutes: number | null;\n};\n\ntype Freewheel = { t0: number; angle0: number; v0: number };\n\nconst ROLE_TEETH: Record<DelegateRole, number> = {\n  admin: 12,\n  editor: 8,\n  viewer: 3,\n};\nconst ROLE_LABEL: Record<DelegateRole, string> = {\n  admin: \"Admin\",\n  editor: \"Editor\",\n  viewer: \"Viewer\",\n};\nconst ROLE_ORDER: DelegateRole[] = [\"admin\", \"editor\", \"viewer\"];\n\nconst DRIVE_TEETH = 24;\nconst IDLER_TEETH = 6; // cancels out of the final ratio — only sets direction + spacing\nconst DEG_PER_SEC = 0.15 * 360; // one shared train phase, 0.15 rev/s\nconst TAU_S = 1.2; // freewheel time constant\nconst SWING_MS = 250; // idler carrier disengage duration\nconst SWING_DEG = 40;\n\nconst MODULE = 2.6; // shared tooth pitch — what lets every gear actually mesh\nconst PAD = 18;\nconst ROW_H = 176;\n\nconst pitchR = (teeth: number) => teeth * MODULE;\nconst rootR = (rOuter: number) => rOuter * 0.62;\nconst boreR = (rInner: number) => rInner * 0.55;\n\nconst DRIVE_R = pitchR(DRIVE_TEETH);\nconst DRIVE_R_IN = rootR(DRIVE_R);\nconst DRIVE_CX = PAD + DRIVE_R;\nconst DRIVE_CY = ROW_H / 2;\n\nconst IDLER_R = pitchR(IDLER_TEETH);\nconst IDLER_R_IN = rootR(IDLER_R);\nconst ARM = DRIVE_R + IDLER_R; // idler's fixed distance from the drive centre\nconst IDLER_CX = DRIVE_CX + ARM;\nconst IDLER_CY = DRIVE_CY;\n\nfunction drivenCx(role: DelegateRole) {\n  return IDLER_CX + IDLER_R + pitchR(ROLE_TEETH[role]);\n}\n\nconst ROW_W = drivenCx(\"admin\") + pitchR(ROLE_TEETH.admin) + PAD;\n\nfunction gearPathAt(\n  cx: number,\n  cy: number,\n  teeth: number,\n  rOuter: number,\n  rInner: number\n): string {\n  const step = 360 / teeth;\n  const half = step * 0.5;\n  const tipHalf = half * 0.5;\n  const toXY = (r: number, deg: number): [number, number] => {\n    const rad = ((deg - 90) * Math.PI) / 180;\n    return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)];\n  };\n  const parts: string[] = [];\n  for (let i = 0; i < teeth; i++) {\n    const c = i * step;\n    const [x0, y0] = toXY(rInner, c - half);\n    const [x1, y1] = toXY(rOuter, c - tipHalf);\n    const [x2, y2] = toXY(rOuter, c + tipHalf);\n    const [x3, y3] = toXY(rInner, c + half);\n    parts.push(`${i === 0 ? \"M\" : \"L\"}${x0.toFixed(2)},${y0.toFixed(2)}`);\n    parts.push(`L${x1.toFixed(2)},${y1.toFixed(2)}`);\n    parts.push(`L${x2.toFixed(2)},${y2.toFixed(2)}`);\n    parts.push(`L${x3.toFixed(2)},${y3.toFixed(2)}`);\n  }\n  parts.push(\"Z\");\n  return parts.join(\" \");\n}\n\n// timing mark: a short radial tick from root to tip at angle 0, so a gear\n// with perfectly regular teeth still reads as rotating rather than static.\nfunction tickAt(cx: number, cy: number, rInner: number, rOuter: number) {\n  return { x1: cx, y1: cy - rInner, x2: cx, y2: cy - rOuter };\n}\n\nconst DRIVE_PATH = gearPathAt(DRIVE_CX, DRIVE_CY, DRIVE_TEETH, DRIVE_R, DRIVE_R_IN);\nconst DRIVE_TICK = tickAt(DRIVE_CX, DRIVE_CY, DRIVE_R_IN, DRIVE_R);\nconst DRIVE_BORE = boreR(DRIVE_R_IN);\n\nconst IDLER_PATH = gearPathAt(IDLER_CX, IDLER_CY, IDLER_TEETH, IDLER_R, IDLER_R_IN);\nconst IDLER_TICK = tickAt(IDLER_CX, IDLER_CY, IDLER_R_IN, IDLER_R);\nconst IDLER_BORE = boreR(IDLER_R_IN);\n\nconst DRIVEN_GEOM = Object.fromEntries(\n  ROLE_ORDER.map((role) => {\n    const cx = drivenCx(role);\n    const r = pitchR(ROLE_TEETH[role]);\n    const rIn = rootR(r);\n    return [\n      role,\n      {\n        cx,\n        cy: DRIVE_CY,\n        r,\n        path: gearPathAt(cx, DRIVE_CY, ROLE_TEETH[role], r, rIn),\n        tick: tickAt(cx, DRIVE_CY, rIn, r),\n        bore: boreR(rIn),\n      },\n    ];\n  })\n) as Record<\n  DelegateRole,\n  { cx: number; cy: number; r: number; path: string; tick: ReturnType<typeof tickAt>; bore: number }\n>;\n\nconst MONTHS = [\n  \"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\",\n  \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\",\n];\nfunction shortDate(ms: number): string {\n  const d = new Date(ms);\n  return `${MONTHS[d.getMonth()]} ${d.getDate()}`;\n}\n\nfunction easeOutCubic(t: number): number {\n  const k = 1 - t;\n  return 1 - k * k * k;\n}\n\nexport function IdlerDrop({\n  delegations,\n  title = \"Delegated admin\",\n  className = \"\",\n}: {\n  delegations: Delegation[];\n  title?: string;\n  className?: string;\n}) {\n  const [rows, setRows] = useState<Row[]>(() =>\n    delegations.map((d) => ({ ...d, revoked: false, coastingMinutes: null }))\n  );\n  const [liveMessage, setLiveMessage] = useState(\"\");\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const rowsRef = useRef(rows);\n  rowsRef.current = rows;\n\n  const gRefs = useRef<\n    Record<string, { drive: SVGGElement | null; idler: SVGGElement | null; driven: SVGGElement | null }>\n  >({});\n  const phaseRef = useRef(0);\n  const freewheelRef = useRef<Record<string, Freewheel>>({});\n  const wakeRef = useRef<(() => void) | null>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const reducedRef = { current: mq.matches };\n    const onMqChange = () => {\n      reducedRef.current = mq.matches;\n      wakeRef.current?.();\n    };\n    mq.addEventListener(\"change\", onMqChange);\n\n    let visible = true;\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible) wakeRef.current?.();\n    });\n    io.observe(root);\n\n    let raf = 0;\n    let last = 0;\n\n    const applyRow = (id: string, driveDeg: number, idlerAttr: string, drivenDeg: number) => {\n      const g = gRefs.current[id];\n      if (!g) return;\n      g.drive?.setAttribute(\"transform\", `rotate(${driveDeg} ${DRIVE_CX} ${DRIVE_CY})`);\n      g.idler?.setAttribute(\"transform\", idlerAttr);\n      const row = rowsRef.current.find((r) => r.id === id);\n      const cx = row ? DRIVEN_GEOM[row.role].cx : 0;\n      g.driven?.setAttribute(\"transform\", `rotate(${drivenDeg} ${cx} ${DRIVE_CY})`);\n    };\n\n    const restFrame = () => {\n      for (const row of rowsRef.current) {\n        const swing = row.revoked ? SWING_DEG : 0;\n        const idlerAttr = `rotate(${swing} ${DRIVE_CX} ${DRIVE_CY}) rotate(0 ${IDLER_CX} ${IDLER_CY})`;\n        applyRow(row.id, 0, idlerAttr, 0);\n      }\n    };\n\n    const loop = (now: number) => {\n      raf = 0;\n      if (reducedRef.current) {\n        restFrame();\n        last = 0;\n        return; // sleeps until the media-query listener wakes it\n      }\n      if (!visible || document.hidden) {\n        last = 0;\n        return; // IntersectionObserver / visibilitychange wakes it\n      }\n      const dt = last === 0 ? 1 / 60 : Math.min(0.05, (now - last) / 1000);\n      last = now;\n      phaseRef.current += DEG_PER_SEC * dt;\n      const phase = phaseRef.current;\n      const idlerSpin = -phase * (DRIVE_TEETH / IDLER_TEETH);\n\n      for (const row of rowsRef.current) {\n        const fw = freewheelRef.current[row.id];\n        const swing = fw\n          ? SWING_DEG * easeOutCubic(Math.min(1, ((now - fw.t0) / SWING_MS)))\n          : 0;\n        const idlerAttr = `rotate(${swing} ${DRIVE_CX} ${DRIVE_CY}) rotate(${idlerSpin} ${IDLER_CX} ${IDLER_CY})`;\n\n        let drivenDeg: number;\n        if (fw) {\n          const t = (now - fw.t0) / 1000;\n          drivenDeg = fw.angle0 + fw.v0 * TAU_S * (1 - Math.exp(-t / TAU_S));\n        } else {\n          drivenDeg = phase * (DRIVE_TEETH / ROLE_TEETH[row.role]);\n        }\n        applyRow(row.id, phase, idlerAttr, drivenDeg);\n      }\n\n      raf = requestAnimationFrame(loop);\n    };\n\n    const wake = () => {\n      if (!raf) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    wakeRef.current = wake;\n    wake();\n\n    const onVis = () => {\n      if (!document.hidden) wake();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      io.disconnect();\n      mq.removeEventListener(\"change\", onMqChange);\n      document.removeEventListener(\"visibilitychange\", onVis);\n      wakeRef.current = null;\n    };\n  }, []);\n\n  function handleRevoke(id: string) {\n    const row = rowsRef.current.find((r) => r.id === id);\n    if (!row || row.revoked) return;\n    const teeth = ROLE_TEETH[row.role];\n    freewheelRef.current[id] = {\n      t0: performance.now(),\n      angle0: phaseRef.current * (DRIVE_TEETH / teeth),\n      v0: DEG_PER_SEC * (DRIVE_TEETH / teeth),\n    };\n    const mins = Math.max(0, Math.round((row.sessionExpiresAt - Date.now()) / 60000));\n    setRows((prev) =>\n      prev.map((r) => (r.id === id ? { ...r, revoked: true, coastingMinutes: mins } : r))\n    );\n    setLiveMessage(\n      `${ROLE_LABEL[row.role]} access revoked for ${row.name}; existing session expires in ${mins} minute${mins === 1 ? \"\" : \"s\"}.`\n    );\n    wakeRef.current?.();\n  }\n\n  function handleRoleChange(id: string, role: DelegateRole) {\n    setRows((prev) => prev.map((r) => (r.id === id && !r.revoked ? { ...r, role } : r)));\n  }\n\n  return (\n    <div ref={rootRef} className={`rounded-md border border-border bg-background ${className}`}>\n      <style>{`\n.ns-idr-btn,.ns-idr-select{transition:opacity 150ms ease-out,border-color 150ms ease-out}\n@media (prefers-reduced-motion: reduce){\n  .ns-idr-btn,.ns-idr-select{transition:none}\n}\n      `}</style>\n\n      <div className=\"border-b border-border px-5 py-4\">\n        <h3 className=\"text-sm font-medium text-foreground\">{title}</h3>\n        <p className=\"mt-1 font-mono text-[11px] tracking-wide text-ns-muted\">\n          drive 24T · admin 12T · editor 8T · viewer 3T\n        </p>\n      </div>\n\n      <div aria-live=\"polite\" role=\"status\" className=\"sr-only\">\n        {liveMessage}\n      </div>\n\n      <ul role=\"list\" className=\"divide-y divide-border\">\n        {rows.map((row, i) => {\n          const geom = DRIVEN_GEOM[row.role];\n          const drivenLeft = (geom.cx / ROW_W) * 100;\n          const drivenTop = (geom.cy / ROW_H) * 100;\n\n          return (\n            <li\n              key={row.id}\n              data-row-index={i}\n              className=\"flex flex-col gap-4 px-5 py-5 sm:flex-row sm:items-center\"\n            >\n              <div\n                className=\"relative w-full shrink-0 sm:w-[220px]\"\n                style={{ aspectRatio: `${ROW_W} / ${ROW_H}` }}\n              >\n                <svg\n                  viewBox={`0 0 ${ROW_W} ${ROW_H}`}\n                  className=\"absolute inset-0 h-full w-full\"\n                  aria-hidden=\"true\"\n                >\n                  <g\n                    ref={(el) => {\n                      gRefs.current[row.id] ??= { drive: null, idler: null, driven: null };\n                      gRefs.current[row.id].drive = el;\n                    }}\n                    transform={`rotate(0 ${DRIVE_CX} ${DRIVE_CY})`}\n                  >\n                    <path d={DRIVE_PATH} fill=\"none\" stroke=\"var(--foreground)\" strokeWidth={1.25} />\n                    <circle cx={DRIVE_CX} cy={DRIVE_CY} r={DRIVE_BORE} fill=\"var(--background)\" stroke=\"var(--border)\" />\n                    <line {...DRIVE_TICK} stroke=\"var(--foreground)\" strokeWidth={1.25} />\n                  </g>\n\n                  <g\n                    ref={(el) => {\n                      gRefs.current[row.id] ??= { drive: null, idler: null, driven: null };\n                      gRefs.current[row.id].idler = el;\n                    }}\n                    transform={`rotate(${row.revoked ? SWING_DEG : 0} ${DRIVE_CX} ${DRIVE_CY}) rotate(0 ${IDLER_CX} ${IDLER_CY})`}\n                  >\n                    <path d={IDLER_PATH} fill=\"none\" stroke=\"var(--ns-muted)\" strokeWidth={1} />\n                    <circle cx={IDLER_CX} cy={IDLER_CY} r={IDLER_BORE} fill=\"var(--background)\" stroke=\"var(--border)\" />\n                    <line {...IDLER_TICK} stroke=\"var(--ns-muted)\" strokeWidth={1} />\n                  </g>\n\n                  <g\n                    ref={(el) => {\n                      gRefs.current[row.id] ??= { drive: null, idler: null, driven: null };\n                      gRefs.current[row.id].driven = el;\n                    }}\n                    transform={`rotate(0 ${geom.cx} ${geom.cy})`}\n                  >\n                    <path d={geom.path} fill=\"none\" stroke=\"var(--foreground)\" strokeWidth={1.25} />\n                    <circle cx={geom.cx} cy={geom.cy} r={geom.bore} fill=\"var(--background)\" stroke=\"var(--border)\" />\n                    <line {...geom.tick} stroke=\"var(--foreground)\" strokeWidth={1.25} />\n                  </g>\n                </svg>\n\n                {/* Real DOM text, anchored at the driven gear's bore — never\n                    part of the rotating group, so it stays upright and\n                    legible while the ring around it turns or freewheels. */}\n                <div\n                  className=\"pointer-events-none absolute select-none text-center leading-tight\"\n                  style={{\n                    left: `${drivenLeft}%`,\n                    top: `${drivenTop}%`,\n                    transform: \"translate(-50%, -50%)\",\n                    width: 64,\n                  }}\n                >\n                  <div className=\"truncate text-[9px] font-semibold text-foreground\">{row.name}</div>\n                  <div className=\"truncate font-mono text-[7.5px] text-ns-muted\">\n                    {ROLE_LABEL[row.role]} · {shortDate(row.grantedAt)}\n                  </div>\n                </div>\n              </div>\n\n              <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-2\">\n                <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-foreground\">\n                  {row.name}\n                </span>\n\n                <label className=\"flex items-center gap-1.5\">\n                  <span className=\"sr-only\">Role for {row.name}</span>\n                  <select\n                    value={row.role}\n                    disabled={row.revoked}\n                    onChange={(e) => handleRoleChange(row.id, e.target.value as DelegateRole)}\n                    aria-label={`Role for ${row.name}`}\n                    className=\"ns-idr-select rounded-sm border border-border bg-background px-2 py-1 text-xs text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent disabled:opacity-45\"\n                  >\n                    {ROLE_ORDER.map((r) => (\n                      <option key={r} value={r}>\n                        {ROLE_LABEL[r]}\n                      </option>\n                    ))}\n                  </select>\n                </label>\n\n                <button\n                  type=\"button\"\n                  data-revoke-btn\n                  disabled={row.revoked}\n                  onClick={() => handleRevoke(row.id)}\n                  aria-label={row.revoked ? `Access revoked for ${row.name}` : `Revoke access for ${row.name}`}\n                  className=\"ns-idr-btn rounded-sm border border-border px-2.5 py-1 text-xs font-medium text-foreground hover:border-foreground/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent disabled:opacity-45 disabled:hover:border-border\"\n                >\n                  {row.revoked ? \"Revoked\" : \"Revoke\"}\n                </button>\n\n                {row.revoked ? (\n                  <span data-coasting className=\"font-mono text-[11px] text-ns-muted\">\n                    coasting: {row.coastingMinutes} min\n                  </span>\n                ) : null}\n              </div>\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/idler-drop.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": [
      "delegation",
      "roles",
      "admin",
      "permissions",
      "security",
      "svg",
      "list",
      "aria-live",
      "accessibility"
    ],
    "instruction": "`<IdlerDrop delegations={Delegation[]} title? className? />` where `Delegation` is `{ id, name, role: \"admin\"|\"editor\"|\"viewer\", grantedAt /* ms epoch */, sessionExpiresAt /* ms epoch — real data the post-revoke readout is computed from */ }`. Renders a `role=\"list\"` of rows; each row is one gear train. MECHANISM: a single shared train phase (module-level DEG_PER_SEC = 0.15 rev/s * 360) is advanced once per animation frame and applied identically to every row's drive gear via `rotate(phase, cx, cy)` on an SVG `<g>` — one clock, so no row's mesh can ever drift out of phase with another's. The drive gear is fixed at 24 teeth. Each delegate's driven gear is fixed at ROLE_TEETH[role] — 12 for admin, 8 for editor, 3 for viewer — because authority comes in exactly three legal roles, not a continuum; a continuously variable radius would visually imply a fractional permission the system has no way to grant. Drive and driven share one tooth module (MODULE = 2.6px/tooth) so every pair of gears in the piece is drawn at a center-distance that actually equals the sum of their pitch radii — the geometry is a real (if stylized) mesh, not two overlapping circles. Between them sits a 6-tooth idler mounted on a carrier arm pivoted at the drive gear's own center, at a fixed radius (rDrive+rIdler) from that pivot — this is what keeps the idler permanently meshed with the drive gear regardless of carrier angle, spinning with it always: the idler is drawn as an extension of YOUR authority, not the delegate's. WHILE ENGAGED (carrier at rest, angle 0) the idler also touches the driven gear, so the driven gear's rotation is a pure function of the shared phase: `drivenDeg = phase * (24 / roleTeeth)` — same direction as the drive gear (two external meshes reverse direction twice), always faster for a smaller-teeth role, i.e. viewer's tiny gear visibly whirs fast while admin's larger gear turns slower — small-and-fast reads as thin/easily-spun authority, big-and-slow reads as weighty. REVOKE swings the carrier by 40° over 250ms (`easeOutCubic`, imperative rAF tween off the click's own timestamp, not a CSS transition — avoids SVG transform-origin inconsistencies) which visibly opens a gap between the idler and the now-stationary-relative driven gear; AT THE SAME INSTANT the driven gear itself leaves the shared-phase formula and starts an independent exponential decay: its angular velocity at the moment of revoke (`v0 = 54deg/s * 24/roleTeeth`) decays as `v0 * exp(-t/1.2s)`, integrated to `angle0 + v0*1.2*(1-exp(-t/1.2))` — so it keeps spinning, slowing continuously, settling over several seconds rather than snapping to a stop. That coast is deliberate: it is the visual disclosure that the delegate's existing session persists past the revoke until its own token actually expires. The drive gear never slows down, never pauses, regardless of how many rows are revoked — it is drawn from the same shared phase as every engaged row, always. IDENTITY TEXT: each driven gear's name / role / grant-date live as real DOM text in a small `<div>` absolutely positioned (as a percentage of the row's SVG viewBox, so it tracks correctly at any render width) at that gear's own center — deliberately OUTSIDE the rotating `<g>`, so the label stays upright and legible while the ring spins or freewheels around it; a thin punched bore circle (`fill: var(--background)`, `stroke: var(--border)`) sits on the rotating layer beneath it for the visual read of a hub. Every gear also carries one radial timing-tick line so a perfectly regular tooth ring still reads as turning frame to frame rather than looking static between tooth-pitch repeats. CONTROLS: each row has a real `<select>` for role (disabled once revoked — the grant is historical, not editable) that changes `ROLE_TEETH[role]` live, immediately re-deriving that row's mesh ratio and geometry; and a real `<button>` labelled Revoke that, once pressed, disables itself, relabels to \"Revoked\", and reveals a `data-coasting` Geist Mono readout — `coasting: 14 min` — computed once from `Math.round((sessionExpiresAt - Date.now()) / 60000)` against the delegate's actual session-expiry timestamp, not a scripted number. A11Y: the whole train SVG is `aria-hidden`; the identity text, role select and revoke button are the real semantic surface. A visually-hidden `role=\"status\" aria-live=\"polite\"` region speaks the coasting fact in full on every revoke — \"Editor access revoked for Sam; existing session expires in 14 minutes.\" — never leaving the freewheel spin as the only signal. REDUCED MOTION: a `matchMedia(\"(prefers-reduced-motion: reduce)\")` check (with a live `change` listener) makes the rAF loop apply one static rest frame instead of scheduling further frames — phase pinned at 0, every engaged driven gear at 0°, and any revoked row's idler carrier still snapped instantly to its swung-out 40° with no tween — so engaged rows read as statically meshed, revoked rows read as statically disengaged, and the coasting label is present and correct either way; only continuous rotation is removed. Colors are `var(--foreground)` for the drive and driven rings/ticks, `var(--ns-muted)` for the idler (the connector, not an authority holder itself) and identity subtext, `var(--border)`/`var(--background)` for the punched bores, and `var(--ns-accent)` solely as the `focus-visible` ring on the select and button — never a resting color. DOM + SVG + CSS only, no canvas, zero dependencies."
  },
  "type": "registry:ui"
}