{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tonearm-skate",
  "title": "Tonearm Skate",
  "description": "A read-only tracking-quality gauge built from real pivoted-tonearm geometry: a headshell sweeps a 215mm-pivot arm across a spinning record from lead-in to run-out while a small error needle reads the actual geometric tracking-error function, centering exactly at the two Baerwald null radii (66mm, 120mm) and drifting off zero everywhere else.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/tonearm-skate/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// TonearmSkate — a read-only tracking-quality gauge built from the actual\n// geometry of a pivoted phono tonearm, not an invented needle sweep. A 9\"\n// arm pivoted 215mm from the spindle (LP_MM) with a 225mm effective\n// (pivot-to-stylus) length traces an arc whose distance from the spindle is\n// r(alpha) = sqrt(Lp^2 + l^2 + 2*Lp*l*cos(alpha)) — the same law-of-cosines\n// relationship that puts a real two-null Baerwald alignment's zero-error\n// points at 66mm and 120mm for this pivot distance. The arm sweeps that\n// radius from a 146mm lead-in down to a 60mm run-out over 48s, then lifts\n// and swings back to the lead-in to start the next \"side\" — unbounded, no\n// dead stop. A separate small tracking-error strip reads a continuously\n// computed geometric value: 0 exactly at the two null radii, growing to\n// roughly +/-2 degrees at the two grooved edges, with a documented (not\n// simulated) interior lobe between the nulls so the needle visibly departs\n// center and returns rather than sitting dead flat for 63% of the sweep.\n// The needle itself never gains weight/opacity from --ns-accent — only\n// stroke-width and opacity scale with |error|, foreground throughout.\n//\n// Disc spins independently of the arm at 1 rev / 4s, continuously, even\n// while the arm is lifted between sides, matching a real motor that never\n// stops. Everything is a single rAF loop mutating refs directly (no\n// per-frame setState) driven off the frame timestamp so a backgrounded tab\n// can't drift the arm out of sync with the disc. Pure SVG + Tailwind token\n// classes (text-foreground / text-border / text-ns-muted) — no raster\n// surface, so no getComputedStyle/MutationObserver token machinery is\n// needed; both themes restyle for free via currentColor.\n// ---------------------------------------------------------------------------\n\n// --- real geometry, millimetres -------------------------------------------\nconst LP_MM = 215; // pivot-to-spindle distance, standard 9\" arm\nconst L_EFF_MM = 225; // effective (pivot-to-stylus) arm length\nconst R_OUTER_MM = 146; // lead-in groove radius\nconst R_INNER_MM = 60; // run-out groove radius\nconst NULL_OUTER_MM = 120; // outer Baerwald null\nconst NULL_INNER_MM = 66; // inner Baerwald null\nconst NULL_MID_MM = (NULL_OUTER_MM + NULL_INNER_MM) / 2; // 93mm, interior lobe\n\n// error-angle keyframes (radius mm, error deg), outer -> inner. Anchored to\n// the real null radii (0 at each) and the documented +/-2deg edge magnitudes;\n// the interior lobe at NULL_MID_MM is set to ~35% of the edge magnitude,\n// matching a real Baerwald curve's interior maximum, so the needle visibly\n// leaves center and returns rather than reading as a flat line between the\n// two nulls (63% of the sweep would otherwise show zero motion).\nconst ERROR_KEYFRAMES: [r: number, deg: number][] = [\n  [R_OUTER_MM, 2.0],\n  [NULL_OUTER_MM, 0],\n  [NULL_MID_MM, -0.7],\n  [NULL_INNER_MM, 0],\n  [R_INNER_MM, -2.0],\n];\nconst SKATE_LEAN_DEG = 3; // constant inward headshell cant, present throughout\n\n// --- timing -----------------------------------------------------------------\nconst DISC_PERIOD_MS = 4_000; // 1 rev / 4s, documented decoupling from 33 1/3 RPM\nconst SWEEP_MS = 48_000; // outer lead-in to inner run-out\nconst HOLD_MS = 700; // brief rest at run-out before lift\nconst LIFT_MS = 1_400; // lift + swing back + set down\nconst SETTLE_MS = 300; // brief rest at lead-in before the next side\nconst CYCLE_MS = SWEEP_MS + HOLD_MS + LIFT_MS + SETTLE_MS;\nconst LIFT_PEAK_VB = 10; // vb units the arm rises at the peak of the lift\n\n// --- scene geometry, viewBox units (vb) -------------------------------------\nconst VB = 300;\nconst RECORD_R_VB = 96;\nconst SCALE = RECORD_R_VB / 150; // mm -> vb, record outer radius is 150mm\nconst SP = { x: 108, y: 150 }; // spindle\nconst LP_VB = LP_MM * SCALE;\nconst L_EFF_VB = L_EFF_MM * SCALE;\nconst PV = { x: SP.x + LP_VB, y: SP.y }; // pivot, off-platter to the right\nconst CW_LEN_VB = 34; // counterweight distance behind the pivot\nconst THREAD_ANCHOR = { x: PV.x - 12, y: PV.y - 14 }; // anti-skate post\n\nfunction toRad(deg: number) {\n  return (deg * Math.PI) / 180;\n}\nfunction toDeg(rad: number) {\n  return (rad * 180) / Math.PI;\n}\nfunction clamp(n: number, lo: number, hi: number) {\n  return Math.max(lo, Math.min(hi, n));\n}\nfunction lerp(a: number, b: number, t: number) {\n  return a + (b - a) * t;\n}\nfunction smoothstep(t: number) {\n  const c = clamp(t, 0, 1);\n  return c * c * (3 - 2 * c);\n}\nfunction easeInOutCubic(t: number) {\n  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;\n}\n\n// arm rotation angle (deg, absolute, measured from the pivot's +x axis with\n// the spindle at angle 180 from it) that puts the stylus at radius r_mm.\n// Derived from r^2 = Lp^2 + l^2 + 2*Lp*l*cos(alpha) — real law-of-cosines\n// geometry, not a fit.\nfunction alphaForRadius(rMm: number) {\n  const r = clamp(rMm, R_INNER_MM, R_OUTER_MM);\n  const cosA = (r * r - LP_MM * LP_MM - L_EFF_MM * L_EFF_MM) / (2 * LP_MM * L_EFF_MM);\n  return toDeg(Math.acos(clamp(cosA, -1, 1)));\n}\n\n// smooth interpolation through ERROR_KEYFRAMES, zero-derivative at each\n// keyframe so a null crossing visibly settles rather than snapping through.\nfunction errorForRadius(rMm: number) {\n  const r = clamp(rMm, R_INNER_MM, R_OUTER_MM);\n  for (let i = 0; i < ERROR_KEYFRAMES.length - 1; i++) {\n    const [r0, e0] = ERROR_KEYFRAMES[i];\n    const [r1, e1] = ERROR_KEYFRAMES[i + 1];\n    if (r <= r0 && r >= r1) {\n      const t = (r0 - r) / (r0 - r1);\n      return lerp(e0, e1, smoothstep(t));\n    }\n  }\n  return ERROR_KEYFRAMES[ERROR_KEYFRAMES.length - 1][1];\n}\n\n// stylus position in vb units for a given absolute arm angle (deg).\nfunction stylusPoint(alphaDeg: number) {\n  const a = toRad(alphaDeg);\n  return {\n    x: PV.x + L_EFF_VB * Math.cos(a),\n    y: PV.y + L_EFF_VB * Math.sin(a),\n  };\n}\n\n// every derived point painted each frame, for a given virtual radius and\n// lift offset — shared by the rAF loop and the static t0 JSX below it, so\n// the two can never drift apart.\nfunction geometryFor(rMm: number, lift: number) {\n  const alpha = alphaForRadius(rMm);\n  const stylus = stylusPoint(alpha);\n  const cwAngle = toRad(alpha + 180);\n  const cw = {\n    x: PV.x + CW_LEN_VB * Math.cos(cwAngle),\n    y: PV.y + CW_LEN_VB * Math.sin(cwAngle),\n  };\n  const threadEnd = {\n    x: PV.x + (stylus.x - PV.x) * 0.72,\n    y: PV.y + (stylus.y - PV.y) * 0.72 - lift,\n  };\n  const tubeExt = {\n    x: stylus.x + 16 * Math.cos(toRad(alpha)),\n    y: stylus.y + 16 * Math.sin(toRad(alpha)),\n  };\n  const headshellRot = alpha + 180 + SKATE_LEAN_DEG;\n  const err = errorForRadius(rMm);\n  const needleX = 130 + (err / 2) * 110;\n  return { alpha, stylus, cw, threadEnd, tubeExt, headshellRot, err, needleX };\n}\n\n// static t0 geometry (arm at the lead-in, no lift) — rendered inline in JSX\n// so the SVG never commits a degenerate (0,0) frame before the first rAF\n// tick, including in the server-rendered / pre-hydration HTML.\nconst T0 = geometryFor(R_OUTER_MM, 0);\n\n// the arm's virtual radius (mm) and lift offset (vb) at elapsed time within\n// one full cycle (sweep, hold, lift-and-return, settle). `airborne` marks\n// the lift phase, where the stylus is off the groove and the tracking-error\n// readout is not a real reading.\nfunction stateForCycleT(cycleMs: number) {\n  if (cycleMs < SWEEP_MS) {\n    return {\n      rMm: lerp(R_OUTER_MM, R_INNER_MM, cycleMs / SWEEP_MS),\n      lift: 0,\n      opacity: 1,\n      airborne: false,\n    };\n  }\n  if (cycleMs < SWEEP_MS + HOLD_MS) {\n    return { rMm: R_INNER_MM, lift: 0, opacity: 1, airborne: false };\n  }\n  if (cycleMs < SWEEP_MS + HOLD_MS + LIFT_MS) {\n    const p = (cycleMs - SWEEP_MS - HOLD_MS) / LIFT_MS;\n    const pe = easeInOutCubic(p);\n    const hump = Math.sin(Math.PI * p);\n    return {\n      rMm: lerp(R_INNER_MM, R_OUTER_MM, pe),\n      lift: LIFT_PEAK_VB * hump,\n      opacity: 1 - 0.12 * hump,\n      airborne: true,\n    };\n  }\n  return { rMm: R_OUTER_MM, lift: 0, opacity: 1, airborne: false };\n}\n\n// the frozen prefers-reduced-motion frame: arm pinned exactly at the first\n// (inner) null, 66mm — needle centered, while a non-zero disc angle shows\n// the disc has visibly turned, so the arm's non-radial rest is legible by\n// contrast rather than looking like a static clock illustration.\nconst REDUCED_R_MM = NULL_INNER_MM;\nconst REDUCED_DISC_DEG = 135;\n\nfunction fmtDeg(n: number) {\n  if (Math.abs(n) < 0.05) return \"0.0°\"; // unsigned at the null, no +/- flicker\n  const s = n > 0 ? \"+\" : \"−\";\n  return `${s}${Math.abs(n).toFixed(1)}°`;\n}\n\nexport interface TonearmSkateProps {\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function TonearmSkate({ className = \"\" }: TonearmSkateProps) {\n  const discRef = useRef<SVGGElement>(null);\n  const armLineRef = useRef<SVGLineElement>(null);\n  const armGroupRef = useRef<SVGGElement>(null);\n  const tubeExtRef = useRef<SVGLineElement>(null);\n  const headshellRef = useRef<SVGRectElement>(null);\n  const stylusDotRef = useRef<SVGCircleElement>(null);\n  const cwRef = useRef<SVGCircleElement>(null);\n  const threadRef = useRef<SVGLineElement>(null);\n  const needleRef = useRef<SVGLineElement>(null);\n  const readoutRef = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let reduced = mq.matches;\n    let raf = 0;\n    let startTs: number | null = null;\n    const onReducedChange = () => {\n      reduced = mq.matches;\n      // the loop stops rescheduling itself while reduced (one static paint\n      // is enough); re-arm it here if motion is turned back on.\n      if (!reduced && raf === 0) {\n        startTs = null;\n        raf = requestAnimationFrame(frame);\n      }\n    };\n    mq.addEventListener(\"change\", onReducedChange);\n\n    function paint(\n      rMm: number,\n      discDeg: number,\n      lift: number,\n      armOpacity: number,\n      airborne: boolean\n    ) {\n      const { stylus, cw, threadEnd, tubeExt, headshellRot, err, needleX } = geometryFor(\n        rMm,\n        lift\n      );\n\n      if (discRef.current) {\n        discRef.current.setAttribute(\"transform\", `rotate(${discDeg} ${SP.x} ${SP.y})`);\n      }\n      if (armGroupRef.current) {\n        armGroupRef.current.setAttribute(\"transform\", `translate(0 ${-lift})`);\n        armGroupRef.current.setAttribute(\"opacity\", String(armOpacity));\n      }\n      if (armLineRef.current) {\n        armLineRef.current.setAttribute(\"x1\", String(PV.x));\n        armLineRef.current.setAttribute(\"y1\", String(PV.y));\n        armLineRef.current.setAttribute(\"x2\", String(stylus.x));\n        armLineRef.current.setAttribute(\"y2\", String(stylus.y));\n      }\n      if (tubeExtRef.current) {\n        tubeExtRef.current.setAttribute(\"x1\", String(stylus.x));\n        tubeExtRef.current.setAttribute(\"y1\", String(stylus.y));\n        tubeExtRef.current.setAttribute(\"x2\", String(tubeExt.x));\n        tubeExtRef.current.setAttribute(\"y2\", String(tubeExt.y));\n      }\n      if (headshellRef.current) {\n        headshellRef.current.setAttribute(\n          \"transform\",\n          `translate(${stylus.x} ${stylus.y}) rotate(${headshellRot})`\n        );\n      }\n      if (stylusDotRef.current) {\n        stylusDotRef.current.setAttribute(\"cx\", String(stylus.x));\n        stylusDotRef.current.setAttribute(\"cy\", String(stylus.y));\n      }\n      if (cwRef.current) {\n        cwRef.current.setAttribute(\"cx\", String(cw.x));\n        cwRef.current.setAttribute(\"cy\", String(cw.y));\n      }\n      if (threadRef.current) {\n        threadRef.current.setAttribute(\"x1\", String(THREAD_ANCHOR.x));\n        threadRef.current.setAttribute(\"y1\", String(THREAD_ANCHOR.y));\n        threadRef.current.setAttribute(\"x2\", String(threadEnd.x));\n        threadRef.current.setAttribute(\"y2\", String(threadEnd.y));\n      }\n\n      // needle strip: error in [-2, 2] deg maps to x in [20, 240], weight\n      // (stroke-width + opacity) scales with |error| / 2 — never accent.\n      // While airborne (lifted between sides) the reading isn't real, so it\n      // fades to near-invisible instead of whipping across the strip.\n      const weight = clamp(Math.abs(err) / 2, 0, 1);\n      if (needleRef.current) {\n        needleRef.current.setAttribute(\"x1\", String(needleX));\n        needleRef.current.setAttribute(\"x2\", String(needleX));\n        needleRef.current.setAttribute(\"stroke-width\", String(1.5 + weight * 1.6));\n        needleRef.current.setAttribute(\"opacity\", String(airborne ? 0.2 : 0.55 + weight * 0.45));\n      }\n      if (readoutRef.current) {\n        readoutRef.current.textContent = airborne ? \"—\" : fmtDeg(err);\n      }\n    }\n\n    function frame(ts: number) {\n      if (reduced) {\n        paint(REDUCED_R_MM, REDUCED_DISC_DEG, 0, 1, false);\n        raf = 0; // repaint on demand only; the change listener re-arms this\n        return;\n      }\n      if (startTs === null) startTs = ts;\n      const elapsed = ts - startTs;\n      const discDeg = ((elapsed % DISC_PERIOD_MS) / DISC_PERIOD_MS) * 360;\n      const cycleT = elapsed % CYCLE_MS;\n      const { rMm, lift, opacity, airborne } = stateForCycleT(cycleT);\n      paint(rMm, discDeg, lift, opacity, airborne);\n      raf = requestAnimationFrame(frame);\n    }\n\n    raf = requestAnimationFrame(frame);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      mq.removeEventListener(\"change\", onReducedChange);\n    };\n  }, []);\n\n  return (\n    <div\n      className={`w-full max-w-sm rounded-md border border-border bg-surface p-4 ${className}`}\n    >\n      <div className=\"mb-3 flex items-baseline justify-between gap-2\">\n        <p className=\"font-mono text-[11px] tracking-widest text-ns-muted\">TONEARM</p>\n        <p className=\"font-mono text-[10px] tracking-widest text-ns-muted\">\n          NULLS {NULL_INNER_MM} / {NULL_OUTER_MM}MM\n        </p>\n      </div>\n\n      <div className=\"relative mx-auto aspect-square w-full max-w-[280px]\">\n        <svg viewBox={`0 0 ${VB} ${VB}`} className=\"h-full w-full\" aria-hidden focusable=\"false\">\n          {/* disc: outer edge, groove-ring ticks, paper label, one foreground\n              notch that is the sole visible proof of rotation */}\n          <g ref={discRef}>\n            <circle\n              cx={SP.x}\n              cy={SP.y}\n              r={RECORD_R_VB}\n              fill=\"none\"\n              className=\"stroke-current text-foreground\"\n              strokeWidth={1.25}\n            />\n            <circle\n              cx={SP.x}\n              cy={SP.y}\n              r={R_OUTER_MM * SCALE}\n              fill=\"none\"\n              className=\"stroke-current text-border\"\n              strokeWidth={0.75}\n            />\n            {/* the two null rings are load-bearing — they're what lets a\n                viewer see the stylus reach a null the instant the needle\n                centers, so they get foreground weight, not border */}\n            <circle\n              cx={SP.x}\n              cy={SP.y}\n              r={NULL_OUTER_MM * SCALE}\n              fill=\"none\"\n              className=\"stroke-current text-foreground\"\n              strokeWidth={0.85}\n              opacity={0.35}\n            />\n            <circle\n              cx={SP.x}\n              cy={SP.y}\n              r={NULL_INNER_MM * SCALE}\n              fill=\"none\"\n              className=\"stroke-current text-foreground\"\n              strokeWidth={0.85}\n              opacity={0.35}\n            />\n            <circle\n              cx={SP.x}\n              cy={SP.y}\n              r={R_INNER_MM * SCALE}\n              fill=\"none\"\n              className=\"stroke-current text-border\"\n              strokeWidth={0.75}\n            />\n            {/* paper label */}\n            <circle\n              cx={SP.x}\n              cy={SP.y}\n              r={20}\n              className=\"fill-current text-ns-muted\"\n              opacity={0.16}\n            />\n            {/* rotation notch — the one asymmetric mark, foreground so it\n                stays visible in light theme */}\n            <line\n              x1={SP.x}\n              y1={SP.y - 20}\n              x2={SP.x}\n              y2={SP.y - 27}\n              className=\"stroke-current text-foreground\"\n              strokeWidth={1.5}\n              strokeLinecap=\"round\"\n            />\n            {/* spindle */}\n            <circle cx={SP.x} cy={SP.y} r={2.5} className=\"fill-current text-foreground\" />\n          </g>\n\n          {/* anti-skate post + thread live outside the lifted group — only\n              the thread's far end (attached to the arm) rises with it, so\n              the post it's anchored to never appears to detach */}\n          <circle\n            cx={THREAD_ANCHOR.x}\n            cy={THREAD_ANCHOR.y}\n            r={2.5}\n            className=\"fill-current text-foreground\"\n            opacity={0.5}\n          />\n          <line\n            ref={threadRef}\n            x1={THREAD_ANCHOR.x}\n            y1={THREAD_ANCHOR.y}\n            x2={T0.threadEnd.x}\n            y2={T0.threadEnd.y}\n            className=\"stroke-current text-foreground\"\n            strokeWidth={0.75}\n            strokeDasharray=\"2 2\"\n            opacity={0.45}\n          />\n          <g ref={armGroupRef}>\n            {/* pivot mount — rigidly attached to the tube, so it lives in\n                the lifted group with everything else that shares the pivot\n                point; only the thread and its post (above) stay fixed */}\n            <circle\n              cx={PV.x}\n              cy={PV.y}\n              r={6}\n              fill=\"none\"\n              className=\"stroke-current text-border\"\n              strokeWidth={1.25}\n            />\n            {/* counterweight */}\n            <circle\n              ref={cwRef}\n              cx={T0.cw.x}\n              cy={T0.cw.y}\n              r={7}\n              className=\"fill-current text-foreground\"\n              opacity={0.85}\n            />\n            {/* arm tube */}\n            <line\n              ref={armLineRef}\n              x1={PV.x}\n              y1={PV.y}\n              x2={T0.stylus.x}\n              y2={T0.stylus.y}\n              className=\"stroke-current text-foreground\"\n              strokeWidth={2.25}\n              strokeLinecap=\"round\"\n            />\n            {/* tube-axis extension, uncanted — diverges visibly from the\n                headshell body below to make the constant 3deg skate lean\n                legible (a single rotated rect alone reads at well under a\n                pixel of displacement) */}\n            <line\n              ref={tubeExtRef}\n              x1={T0.stylus.x}\n              y1={T0.stylus.y}\n              x2={T0.tubeExt.x}\n              y2={T0.tubeExt.y}\n              className=\"stroke-current text-foreground\"\n              strokeWidth={0.75}\n              opacity={0.3}\n            />\n            {/* headshell, canted by the constant skate lean */}\n            <rect\n              ref={headshellRef}\n              x={-4}\n              y={-3}\n              width={22}\n              height={6}\n              rx={1.25}\n              transform={`translate(${T0.stylus.x} ${T0.stylus.y}) rotate(${T0.headshellRot})`}\n              className=\"fill-current text-foreground\"\n            />\n            {/* stylus / contact point */}\n            <circle\n              ref={stylusDotRef}\n              cx={T0.stylus.x}\n              cy={T0.stylus.y}\n              r={1.75}\n              className=\"fill-current text-foreground\"\n            />\n          </g>\n        </svg>\n      </div>\n\n      <div className=\"mt-4 flex items-baseline justify-between gap-3\">\n        <p className=\"font-mono text-[11px] tracking-widest text-ns-muted\">TRACKING ERROR</p>\n        <span\n          ref={readoutRef}\n          className=\"font-mono text-[11px] font-semibold tabular-nums text-foreground\"\n        >\n          {fmtDeg(T0.err)}\n        </span>\n      </div>\n\n      <svg viewBox=\"0 0 260 24\" className=\"mt-1 h-6 w-full\" aria-hidden focusable=\"false\">\n        <line x1={20} x2={240} y1={12} y2={12} className=\"stroke-current text-border\" strokeWidth={1} />\n        <line x1={20} x2={20} y1={7} y2={17} className=\"stroke-current text-border\" strokeWidth={1} />\n        <line x1={240} x2={240} y1={7} y2={17} className=\"stroke-current text-border\" strokeWidth={1} />\n        <line x1={130} x2={130} y1={5} y2={19} className=\"stroke-current text-foreground\" strokeWidth={1} opacity={0.4} />\n        <line\n          ref={needleRef}\n          x1={T0.needleX}\n          x2={T0.needleX}\n          y1={4}\n          y2={20}\n          strokeWidth={1.5 + clamp(Math.abs(T0.err) / 2, 0, 1) * 1.6}\n          className=\"stroke-current text-foreground\"\n          strokeLinecap=\"round\"\n        />\n      </svg>\n\n      <p className=\"mt-2 font-mono text-[10px] leading-relaxed text-ns-muted\">\n        lead-in {R_OUTER_MM}mm &middot; run-out {R_INNER_MM}mm &mdash; error crosses zero at each\n        null, growing toward both edges\n      </p>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/tonearm-skate.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": [
      "gauge",
      "meter",
      "svg",
      "rotary",
      "geometry",
      "instrument",
      "quality",
      "tracking",
      "ambient",
      "status"
    ],
    "instruction": "Renders one autonomous, data-free instrument (no props drive its motion) built on the real geometry of a pivoted phono tonearm: LP_MM=215 (pivot-to-spindle) and L_EFF_MM=225 (effective pivot-to-stylus length) are real constants for a standard 9\" arm, and the arm's absolute rotation angle for any target stylus radius r is derived from the true law-of-cosines relationship r^2 = Lp^2 + l^2 + 2*Lp*l*cos(alpha) (alphaForRadius, solved via acos, not fit or approximated) — the same geometry that places this pivot distance's two Baerwald alignment nulls at 66mm and 120mm. A single rAF loop (no per-frame setState; every mutation is a direct ref.setAttribute call) derives an elapsed-time value from the frame's own timestamp (never accumulated per-frame deltas, so a backgrounded tab cannot drift the arm out of sync with the disc) and drives two independent clocks off it: the record disc rotates continuously at 1 rev/4000ms via a rotate() transform on a <g> around the spindle point SP, regardless of what the arm is doing (a real turntable motor never stops between sides); and the arm runs a 50400ms cycle (stateForCycleT) — a 48000ms linear sweep of its target radius from the 146mm lead-in groove down to the 60mm run-out groove, a 700ms hold at run-out, a 1400ms eased lift-and-return (easeInOutCubic on the radius, plus a sin(pi*p)-shaped vertical hop capped at LIFT_PEAK_VB and a matching opacity dip, so the pickup/swing/set-down reads as continuous motion, never a jump-cut) back to the lead-in, and a 300ms settle before the next side begins — unbounded, no dead stop. alphaForRadius(targetRadius) converts that virtual radius into the arm's absolute pivot angle every frame; stylusPoint(alpha) places the stylus in scene coordinates; the counterweight (a filled circle rigidly opposite the headshell across the pivot at a fixed CW_LEN_VB radius) and a dashed anti-skate thread (from a fixed post near the pivot to a point 72% along the current arm-tube line) are recomputed from that same alpha every frame, so they swing in lockstep with the arm rather than being static decoration. The headshell <rect> is rotated to the tube's angle plus a constant SKATE_LEAN_DEG=3 cant that never changes with value or time — the skating force is a standing offset, not a discrete event — while the small stylus-contact <circle> stays exactly on the tube's true endpoint, unrotated by the lean, since that circle is the actual groove-contact point. A second, independently driven readout is the tracking-error strip below the disc: errorForRadius(r) walks a five-point keyframe table (146mm:+2deg, 120mm:0, 93mm:-0.7deg, 66mm:0, 60mm:-2deg — anchored to the real null radii and the documented +/-2deg edge magnitudes, with the 93mm interior lobe set to roughly a third of the edge magnitude so the needle visibly leaves and returns to center between the nulls rather than reading as a flat line for the 63% of the sweep that separates them) and interpolates between adjacent keyframes with a smoothstep(t) (zero derivative at every keyframe, so the needle visibly decelerates into and pauses at each null before departing again — that pause is what makes the crossing legible as 'it centered' rather than a blip) rather than a straight lerp. The needle itself is a single <line> whose x position maps error from [-2,2]deg linearly to viewBox x in [20,240]; only its stroke-width (1.5 to 3.1) and opacity (0.55 to 1.0) scale with |error|/2 — it is stroke-current text-foreground always, never text-ns-accent, since a higher error reads as heavier/darker, not as interaction chrome. Disc, arm tube, headshell, counterweight, stylus dot and the fixed pivot mount are all stroke-current or fill-current classes on text-foreground; the disc's groove-ring circles (at the lead-in, both null, and run-out radii) and the anti-skate thread are text-border — pure separator use, never load-bearing fill; the paper label circle is text-ns-muted at low opacity. The disc's rotation would otherwise be visually silent (four concentric circles have full rotational symmetry) so a single short radial notch just outside the paper label is drawn text-foreground specifically so it stays visible against a light theme and is the one asymmetric mark that proves the disc is turning. Under prefers-reduced-motion (matchMedia with a live 'change' listener; the rAF loop keeps running so the disc-independent-of-arm relationship never has to be reasoned about statically) the whole scene freezes on a deliberately chosen non-t0 frame: the arm pinned at exactly the 66mm inner null (needle reading 0.0deg dead center) while the disc is held at a fixed 135deg rotation rather than its start angle — chosen because that single frame shows both 'the geometry is exactly correct here' (centered needle) and, by the arm's visibly non-radial position against an already-turned disc, that error exists everywhere else along the sweep. Geometry is derived from the SVG's own viewBox (300x300, fixed internal units) scaled from real millimetres via SCALE = 96/150 (record radius in viewBox units over the real 150mm LP radius), and the whole scene sits in a wrapper with aspect-square and max-w-[280px] so it reads at card scale regardless of container width. Every point painted per frame (stylus, counterweight, anti-skate thread end, tube-axis extension, headshell rotation, needle x, numeric error) comes from one `geometryFor(radius, lift)` function shared by the rAF loop and a module-scope `T0 = geometryFor(R_OUTER_MM, 0)` constant that is inlined directly as the initial JSX attributes on every ref'd element (arm tube, headshell transform, stylus dot, counterweight, thread, needle, readout text) — without it, every ref'd SVG primitive would default to (0,0)/zero-length until the first rAF tick, and since these are client components under App Router that wrong frame would sit in the pre-hydration server HTML, not just flash for one frame; this is the geometric form of the binding rule against painting before the first read. No canvas anywhere, no color literal anywhere, no getComputedStyle/MutationObserver machinery — nothing here is a raster surface, every paint is a token Tailwind class, so a theme flip repaints for free. Cleanup cancels the single rAF and removes the matchMedia listener on unmount. The root element carries no role/aria-label of its own — every SVG is aria-hidden, but the visible plain-text label row ('TONEARM' / null radii), the live numeric error readout (a plain HTML span, not SVG text, updated via textContent each frame), and the lead-in/run-out/null caption below the disc are ordinary DOM text nodes an AT already reads correctly; wrapping the root in role=img would collapse all of that into a single opaque string, which is why it's deliberately absent (matching barograph-drum-week, which also leaves its root unlabelled and aria-hides only the non-text layers). Props are `className` only — this is an autonomous instrument, not a data-bound meter; it takes no `value` and exposes no drag interaction (the spec's optional scrub-to-inspect affordance was deliberately dropped, since it is the only path by which text-ns-accent could leak onto the needle, and the passive resting loop already carries the full legibility requirement on its own). The anti-skate thread and its post, and the pivot mount circle, are rendered as fixed siblings outside the lifted arm group — only the thread's far end (recomputed from the arm's live position minus the current lift offset) moves, so the thread never visibly detaches from its anchor during the lift-and-return phase. The 3deg skate lean would be sub-pixel on the headshell rect alone, so a second, uncanted hairline continues the true tube axis a further 16vb past the stylus — the two nearly-parallel lines' visible divergence is what makes the constant lean legible, not the rect's rotation by itself. During the 1400ms lift-and-return phase (airborne=true) the tracking-error needle fades to a fixed 0.2 opacity and the numeric readout shows an em dash instead of a number, since the stylus is off the groove and the formula's output during that phase is not a real reading — without that suppression the return sweep would manufacture two fast, meaningless null-crossing-looking events every 50.4s loop. Under prefers-reduced-motion the rAF loop paints the frozen frame once and then stops rescheduling itself (no wasted per-frame work animating an unchanging scene); the matchMedia 'change' listener re-arms a fresh rAF loop if motion is turned back on mid-session."
  },
  "type": "registry:ui"
}