{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cmm-probe-touch",
  "title": "CMM Probe Touch",
  "description": "An empty-state ambient reproducing coordinate-measuring machine touch-trigger probing: a stylus indexes to 18 stations around a fixed part outline, approaches slowly along the local surface normal, dwells at contact for a luminance-only trigger flash, retracts, and travels to the next station, with older touched points rolling off a one-lap trailing fade so the inspection is always mid-pass, never finished.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/cmm-probe-touch/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// CmmProbeTouch — an empty-state ambient built on coordinate-measuring\n// machine (CMM) touch-trigger probing, standard contact metrology. A probe\n// tip indexes to a programmed station on a part's outline, approaches\n// slowly along the LOCAL SURFACE NORMAL, dwells at contact (the trigger\n// fires — a luminance flash, never a colour change), retracts, and travels\n// to the next station — an inspection pass that never finishes, because a\n// CMM program loops the same routine part after part.\n//\n// The part is a single fixed closed contour (rounded-rect silhouette with a\n// notch and a boss cut in) built once from an explicit vertex list —\n// straight edges plus subdivided corner/boss arcs — then walked into one\n// dense, evenly-arc-length-parameterised polyline. STATIONS = 18 stations\n// sit at even arc-length fractions of that polyline; each station's local\n// outward normal comes from the tangent of its two neighbours on the dense\n// polyline, sign-checked against the contour's centroid so it always points\n// away from the part.\n//\n// Per-station cycle is a literal 1.2s state machine (420ms approach / 180ms\n// dwell / 300ms retract / 300ms travel-to-next) driven off ONE monotonic\n// virtual clock — no per-station timers, no array of \"in-flight\" probes.\n// Touched-point age is read analytically off that same clock: station k's\n// most recent contact time is `k*1200 + APPROACH_MS` mod the 21.6s lap, so\n// `age = (nowInLap - thatTime + LAP) % LAP` is always \"time since k was\n// last touched\", whether that touch was earlier this lap or the previous\n// one — no history array, no per-station timestamp bookkeeping. The clock\n// starts pre-seeded a full lap plus a fractional offset ahead of zero, so\n// every station already has a valid touch age at t0 (a rolling trail with\n// history, never a blank contour) and the lap keeps indexing forever.\n// ---------------------------------------------------------------------------\n\ninterface Vec {\n  x: number;\n  y: number;\n}\n\nconst STATIONS = 18;\nconst APPROACH_MS = 420;\nconst DWELL_MS = 180;\nconst RETRACT_MS = 300;\nconst TRAVEL_MS = 300;\nconst STATION_MS = APPROACH_MS + DWELL_MS + RETRACT_MS + TRAVEL_MS; // 1200\nconst LAP_MS = STATION_MS * STATIONS; // 21600\nconst SEED_LAP_FRACTION = 0.42; // t0 phase within the pre-seeded lap\n\n// local shape constants (unitless, scaled by the container's smaller dimension)\nconst HX = 0.82;\nconst HY = 0.5;\nconst CORNER_R = 0.12;\nconst BOSS_R = 0.15;\nconst NOTCH_HALF = 0.15;\nconst NOTCH_DEPTH = 0.16;\nconst ARC_STEPS = 14;\n\nconst APPROACH_DIST = 0.24; // station normal offset, in the same local units\nconst NORMAL_SAMPLE_DELTA = 0.004; // arc-length fraction used for tangent finite-difference\n\nconst FREEZE_STATION = 9;\nconst FREEZE_PHASE = \"contact-dwell-station9\";\n\nfunction arcPoints(cx: number, cy: number, r: number, a0: number, a1: number, steps: number): Vec[] {\n  const pts: Vec[] = [];\n  for (let i = 0; i <= steps; i++) {\n    const t = a0 + (a1 - a0) * (i / steps);\n    pts.push({ x: cx + r * Math.cos(t), y: cy + r * Math.sin(t) });\n  }\n  return pts;\n}\n\n/** Explicit vertex walk of the part silhouette: top edge with a rectangular\n * notch, rounded top-right corner, right edge with a semicircular boss cut\n * outward, rounded bottom-right corner, bottom edge, rounded bottom-left\n * corner, left edge, rounded top-left corner, closing back to start. */\nfunction buildOutline(): Vec[] {\n  const pts: Vec[] = [];\n  const push = (p: Vec) => {\n    const prev = pts[pts.length - 1];\n    if (!prev || Math.hypot(p.x - prev.x, p.y - prev.y) > 1e-6) pts.push(p);\n  };\n\n  push({ x: -HX + CORNER_R, y: -HY });\n  push({ x: -NOTCH_HALF, y: -HY });\n  push({ x: -NOTCH_HALF, y: -HY + NOTCH_DEPTH });\n  push({ x: NOTCH_HALF, y: -HY + NOTCH_DEPTH });\n  push({ x: NOTCH_HALF, y: -HY });\n  push({ x: HX - CORNER_R, y: -HY });\n  for (const p of arcPoints(HX - CORNER_R, -HY + CORNER_R, CORNER_R, -Math.PI / 2, 0, ARC_STEPS)) push(p);\n  push({ x: HX, y: -BOSS_R });\n  for (const p of arcPoints(HX, 0, BOSS_R, -Math.PI / 2, Math.PI / 2, ARC_STEPS)) push(p);\n  push({ x: HX, y: HY - CORNER_R });\n  for (const p of arcPoints(HX - CORNER_R, HY - CORNER_R, CORNER_R, 0, Math.PI / 2, ARC_STEPS)) push(p);\n  push({ x: -HX + CORNER_R, y: HY });\n  for (const p of arcPoints(-HX + CORNER_R, HY - CORNER_R, CORNER_R, Math.PI / 2, Math.PI, ARC_STEPS)) push(p);\n  push({ x: -HX, y: -HY + CORNER_R });\n  for (const p of arcPoints(-HX + CORNER_R, -HY + CORNER_R, CORNER_R, Math.PI, (3 * Math.PI) / 2, ARC_STEPS)) push(p);\n  return pts;\n}\n\n/** A closed polyline resampled into a lookup usable at any arc-length\n * fraction s in [0,1) via linear interpolation between its cumulative\n * lengths — the single source of truth for both the drawn contour and the\n * evenly-spaced station positions. */\nclass ArcPath {\n  private readonly pts: Vec[];\n  private readonly cum: number[];\n  readonly total: number;\n  readonly centroid: Vec;\n\n  constructor(pts: Vec[]) {\n    this.pts = pts;\n    const cum = [0];\n    let sum = 0;\n    let cx = 0;\n    let cy = 0;\n    for (let i = 0; i < pts.length; i++) {\n      const a = pts[i]!;\n      const b = pts[(i + 1) % pts.length]!;\n      sum += Math.hypot(b.x - a.x, b.y - a.y);\n      cum.push(sum);\n      cx += a.x;\n      cy += a.y;\n    }\n    this.cum = cum;\n    this.total = sum;\n    this.centroid = { x: cx / pts.length, y: cy / pts.length };\n  }\n\n  at(sFrac: number): Vec {\n    const n = this.pts.length;\n    let s = ((sFrac % 1) + 1) % 1;\n    const target = s * this.total;\n    let lo = 0;\n    let hi = n - 1;\n    while (lo < hi) {\n      const mid = (lo + hi + 1) >> 1;\n      if (this.cum[mid]! <= target) lo = mid;\n      else hi = mid - 1;\n    }\n    const a = this.pts[lo]!;\n    const b = this.pts[(lo + 1) % n]!;\n    const segLen = this.cum[lo + 1]! - this.cum[lo]!;\n    const t = segLen > 1e-9 ? (target - this.cum[lo]!) / segLen : 0;\n    return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };\n  }\n\n  normalAt(sFrac: number): Vec {\n    const a = this.at(sFrac - NORMAL_SAMPLE_DELTA);\n    const b = this.at(sFrac + NORMAL_SAMPLE_DELTA);\n    const dx = b.x - a.x;\n    const dy = b.y - a.y;\n    const len = Math.hypot(dx, dy) || 1;\n    let nx = -dy / len;\n    let ny = dx / len;\n    const p = this.at(sFrac);\n    const outSign = (p.x - this.centroid.x) * nx + (p.y - this.centroid.y) * ny;\n    if (outSign < 0) {\n      nx = -nx;\n      ny = -ny;\n    }\n    return { x: nx, y: ny };\n  }\n}\n\nfunction easeOutCubic(t: number): number {\n  const u = 1 - t;\n  return 1 - u * u * u;\n}\n\nfunction easeInCubic(t: number): number {\n  return t * t * t;\n}\n\nfunction easeInOutCubic(t: number): number {\n  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;\n}\n\nfunction clamp01(x: number): number {\n  return x < 0 ? 0 : x > 1 ? 1 : x;\n}\n\n/** deterministic small-int hash -> a plausible synthetic deviation reading\n * in mm, stable per station index across renders and reduced-motion. */\nfunction syntheticDeviation(i: number): string {\n  const h = Math.sin(i * 12.9898) * 43758.5453;\n  const frac = h - Math.floor(h);\n  const mm = (frac - 0.5) * 0.036;\n  const sign = mm >= 0 ? \"+\" : \"-\";\n  return `${sign}${Math.abs(mm).toFixed(3)}mm`;\n}\n\nexport interface CmmProbeTouchProps {\n  /** icon/card box size in px (square) */\n  size?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function CmmProbeTouch({ size = 220, className = \"\" }: CmmProbeTouchProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const [activeStation, setActiveStation] = useState<number | null>(null);\n  const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>(null);\n\n  const pathRef = useRef<ArcPath | null>(null);\n  if (!pathRef.current) pathRef.current = new ArcPath(buildOutline());\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    if (!root || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const path = pathRef.current!;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let fg = \"currentColor\";\n    let muted = \"currentColor\";\n    let border = \"currentColor\";\n\n    const readTokens = () => {\n      const s = getComputedStyle(document.documentElement);\n      fg = s.getPropertyValue(\"--foreground\").trim() || \"currentColor\";\n      muted = s.getPropertyValue(\"--ns-muted\").trim() || fg;\n      border = s.getPropertyValue(\"--border\").trim() || muted;\n    };\n\n    let width = 0;\n    let height = 0;\n    let dpr = 1;\n    let sized = false;\n    let disposed = false;\n    let visible = true;\n\n    let scale = 1;\n    let originX = 0;\n    let originY = 0;\n\n    const toScreen = (p: Vec): Vec => ({ x: originX + p.x * scale, y: originY + p.y * scale });\n\n    let raf = 0;\n    let last = 0;\n    let elapsed = LAP_MS + SEED_LAP_FRACTION * LAP_MS; // pre-seeded virtual clock, ms\n\n    const stationTouchTime = (k: number) => k * STATION_MS + APPROACH_MS;\n\n    const stationAge = (k: number, nowMs: number): number => {\n      const nowInLap = ((nowMs % LAP_MS) + LAP_MS) % LAP_MS;\n      const touch = stationTouchTime(k);\n      return ((nowInLap - touch + LAP_MS) % LAP_MS) || 0;\n    };\n\n    const stationScreenPoint = (k: number): Vec => toScreen(path.at(k / STATIONS));\n    const stationApproachPoint = (k: number): Vec => {\n      const p = path.at(k / STATIONS);\n      const n = path.normalAt(k / STATIONS);\n      return toScreen({ x: p.x + n.x * APPROACH_DIST, y: p.y + n.y * APPROACH_DIST });\n    };\n\n    const drawContour = () => {\n      ctx.beginPath();\n      const n = 200;\n      for (let i = 0; i <= n; i++) {\n        const p = toScreen(path.at(i / n));\n        if (i === 0) ctx.moveTo(p.x, p.y);\n        else ctx.lineTo(p.x, p.y);\n      }\n      ctx.closePath();\n      ctx.strokeStyle = border;\n      ctx.lineWidth = Math.max(1, scale * 0.006);\n      ctx.stroke();\n    };\n\n    const dotR = () => Math.max(1.6, scale * 0.02);\n\n    const drawTouchedPoints = (nowMs: number) => {\n      const r = dotR();\n      // base pass: every station drawn once in --ns-muted at full alpha\n      ctx.fillStyle = muted;\n      for (let k = 0; k < STATIONS; k++) {\n        const p = stationScreenPoint(k);\n        ctx.beginPath();\n        ctx.arc(p.x, p.y, r, 0, Math.PI * 2);\n        ctx.fill();\n      }\n      // overlay pass: --foreground crossfaded in by how recently each\n      // station was touched — alpha ramp only, never string arithmetic\n      ctx.fillStyle = fg;\n      for (let k = 0; k < STATIONS; k++) {\n        const age = stationAge(k, nowMs);\n        const alpha = clamp01(1 - age / LAP_MS);\n        if (alpha <= 0.02) continue;\n        const p = stationScreenPoint(k);\n        ctx.globalAlpha = alpha;\n        ctx.beginPath();\n        ctx.arc(p.x, p.y, r, 0, Math.PI * 2);\n        ctx.fill();\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    const drawProbe = (nowMs: number) => {\n      const nowInLap = ((nowMs % LAP_MS) + LAP_MS) % LAP_MS;\n      const k = Math.floor(nowInLap / STATION_MS) % STATIONS;\n      const within = nowInLap - k * STATION_MS;\n      const stationS = k / STATIONS;\n      const contact = path.at(stationS);\n      const normal = path.normalAt(stationS);\n      const contactPt = toScreen(contact);\n      const approachPt = toScreen({ x: contact.x + normal.x * APPROACH_DIST, y: contact.y + normal.y * APPROACH_DIST });\n\n      let tip: Vec;\n      let flashT = 0;\n\n      if (within < APPROACH_MS) {\n        const t = easeOutCubic(within / APPROACH_MS);\n        tip = { x: approachPt.x + (contactPt.x - approachPt.x) * t, y: approachPt.y + (contactPt.y - approachPt.y) * t };\n      } else if (within < APPROACH_MS + DWELL_MS) {\n        tip = contactPt;\n        const dt = (within - APPROACH_MS) / DWELL_MS;\n        flashT = Math.sin(dt * Math.PI); // 0 -> 1 -> 0 across the dwell window\n      } else if (within < APPROACH_MS + DWELL_MS + RETRACT_MS) {\n        const t = easeInCubic((within - APPROACH_MS - DWELL_MS) / RETRACT_MS);\n        tip = { x: contactPt.x + (approachPt.x - contactPt.x) * t, y: contactPt.y + (approachPt.y - contactPt.y) * t };\n      } else {\n        const nextK = (k + 1) % STATIONS;\n        const nextS = nextK / STATIONS;\n        const nextContact = path.at(nextS);\n        const nextNormal = path.normalAt(nextS);\n        const nextApproach = toScreen({\n          x: nextContact.x + nextNormal.x * APPROACH_DIST,\n          y: nextContact.y + nextNormal.y * APPROACH_DIST,\n        });\n        const t = easeInOutCubic((within - APPROACH_MS - DWELL_MS - RETRACT_MS) / TRAVEL_MS);\n        tip = { x: approachPt.x + (nextApproach.x - approachPt.x) * t, y: approachPt.y + (nextApproach.y - approachPt.y) * t };\n      }\n\n      // stylus shaft: a short segment trailing outward from the tip along\n      // the current station's normal, so the probe reads as a stylus, not\n      // a bare dot\n      const shaftLen = scale * 0.14;\n      const shaftEnd = { x: tip.x + normal.x * shaftLen, y: tip.y + normal.y * shaftLen };\n      ctx.strokeStyle = fg;\n      ctx.lineWidth = Math.max(1, scale * 0.01);\n      ctx.globalAlpha = 0.6;\n      ctx.beginPath();\n      ctx.moveTo(tip.x, tip.y);\n      ctx.lineTo(shaftEnd.x, shaftEnd.y);\n      ctx.stroke();\n      ctx.globalAlpha = 1;\n\n      const baseR = Math.max(1.8, scale * 0.024);\n      if (flashT > 0.01) {\n        // trigger flash: a luminance bump (larger radius, brighter\n        // overlay), never a colour tint\n        ctx.fillStyle = fg;\n        ctx.globalAlpha = 1;\n        ctx.beginPath();\n        ctx.arc(tip.x, tip.y, baseR * (1 + 0.9 * flashT), 0, Math.PI * 2);\n        ctx.fill();\n        ctx.globalAlpha = 0.35 * flashT;\n        ctx.beginPath();\n        ctx.arc(tip.x, tip.y, baseR * (1 + 2.2 * flashT), 0, Math.PI * 2);\n        ctx.fill();\n        ctx.globalAlpha = 1;\n      } else {\n        ctx.fillStyle = fg;\n        ctx.beginPath();\n        ctx.arc(tip.x, tip.y, baseR, 0, Math.PI * 2);\n        ctx.fill();\n      }\n    };\n\n    const render = (nowMs: number) => {\n      if (!sized) return;\n      ctx.clearRect(0, 0, width, height);\n      drawContour();\n      drawTouchedPoints(nowMs);\n      drawProbe(nowMs);\n    };\n\n    const positionOverlay = () => {\n      for (let k = 0; k < STATIONS; k++) {\n        const btn = buttonRefs.current[k];\n        if (!btn) continue;\n        const p = stationScreenPoint(k);\n        btn.style.left = `${p.x}px`;\n        btn.style.top = `${p.y}px`;\n      }\n    };\n\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect();\n      width = rect.width;\n      height = rect.height;\n      if (width < 2 || height < 2) {\n        sized = false;\n        return;\n      }\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(width * dpr));\n      canvas.height = Math.max(1, Math.round(height * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      const minDim = Math.min(width, height);\n      scale = minDim * 0.42;\n      originX = width / 2;\n      originY = height / 2;\n      sized = true;\n      positionOverlay();\n      render(elapsed);\n    };\n\n    let resizeTimer = 0;\n    const onResize = () => {\n      window.clearTimeout(resizeTimer);\n      resizeTimer = window.setTimeout(resize, 100);\n    };\n    const ro = new ResizeObserver(onResize);\n    ro.observe(canvas);\n\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible && !reduced && !raf) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    });\n    io.observe(canvas);\n\n    function loop(now: number) {\n      raf = 0;\n      if (!visible || document.hidden) return;\n      if (last === 0) last = now;\n      const dt = Math.min(100, now - last);\n      last = now;\n      elapsed += dt;\n      render(elapsed);\n      raf = requestAnimationFrame(loop);\n    }\n\n    const onVis = () => {\n      if (!document.hidden && visible && !reduced && !raf) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    const mo = new MutationObserver(() => {\n      readTokens();\n      render(elapsed);\n    });\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n\n    document.fonts.ready.then(() => {\n      if (disposed) return;\n      readTokens();\n      resize();\n      if (reduced) {\n        elapsed = LAP_MS + FREEZE_STATION * STATION_MS + APPROACH_MS + DWELL_MS / 2;\n        render(elapsed);\n      } else {\n        raf = requestAnimationFrame(loop);\n      }\n    });\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      raf = 0;\n      window.clearTimeout(resizeTimer);\n      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [size]);\n\n  const showTooltip = (k: number) => {\n    const btn = buttonRefs.current[k];\n    const root = rootRef.current;\n    if (!btn || !root) return;\n    const rootRect = root.getBoundingClientRect();\n    const btnRect = btn.getBoundingClientRect();\n    setActiveStation(k);\n    setTooltipPos({ x: btnRect.left - rootRect.left + btnRect.width / 2, y: btnRect.top - rootRect.top });\n  };\n  const hideTooltip = () => {\n    setActiveStation(null);\n    setTooltipPos(null);\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      className={`relative ${className}`}\n      style={{ width: size, height: size }}\n      data-reduced-motion-freeze={FREEZE_PHASE}\n    >\n      <canvas ref={canvasRef} aria-hidden=\"true\" className=\"block h-full w-full\" />\n      {Array.from({ length: STATIONS }, (_, k) => (\n        <button\n          key={k}\n          ref={(el) => {\n            buttonRefs.current[k] = el;\n          }}\n          type=\"button\"\n          aria-label={`Inspection station ${k + 1} deviation ${syntheticDeviation(k)}`}\n          className=\"absolute h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ns-accent focus-visible:ring-offset-1\"\n          onMouseEnter={() => showTooltip(k)}\n          onMouseLeave={hideTooltip}\n          onFocus={() => showTooltip(k)}\n          onBlur={hideTooltip}\n        />\n      ))}\n      {activeStation !== null && tooltipPos && (\n        <div\n          role=\"tooltip\"\n          className=\"pointer-events-none absolute z-10 -translate-x-1/2 -translate-y-full whitespace-nowrap rounded-sm border border-border bg-background px-2 py-1 font-mono text-[10px] text-ns-muted\"\n          style={{ left: tooltipPos.x, top: tooltipPos.y - 6 }}\n        >\n          station {activeStation + 1} dev {syntheticDeviation(activeStation)}\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/cmm-probe-touch.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": [
      "empty-state",
      "canvas",
      "metrology",
      "cmm",
      "process",
      "monochrome",
      "hover",
      "ambient"
    ],
    "instruction": "An empty-state ambient built on real CMM (coordinate-measuring machine) touch-trigger probing, standard contact metrology, rendered on a single 2D canvas sized off the container's own `getBoundingClientRect` (DPR-capped at 2). The part is a single fixed closed contour built once from an explicit vertex walk in local unit space (half-extents HX=0.82, HY=0.5): a rounded-rectangle-ish silhouette with a rectangular notch cut into the top edge (half-width 0.15, depth 0.16) and a semicircular boss (radius 0.15) bulging outward from the right edge, corners rounded at radius 0.12 with 14-segment arcs. That vertex list is wrapped in an `ArcPath` helper that walks it into cumulative arc length and exposes `at(sFraction)` (binary-searched linear interpolation along the closed loop) and `normalAt(sFraction)` (central-difference tangent at sFraction +/- 0.004, rotated 90 degrees, sign-checked against the contour's centroid so it always points outward) — this same lookup drives both the drawn contour stroke and every station position, so there is exactly one source of geometric truth. STATIONS = 18 stations sit at even arc-length fractions k/18.\n\nAnimation is driven by one monotonic virtual clock (`elapsed`, ms, advanced by real rAF delta so speed never depends on frame rate), pre-seeded at mount to one full lap (21600ms) plus 0.42 of a lap, so every station already has a valid touch history at t0 — a rolling trail with partial composition visible immediately, never a blank contour. Per-station cycle is a fixed 1200ms state machine read off `elapsed mod 1200`: 0-420ms approach (ease-out-cubic from a point offset `APPROACH_DIST=0.24` local units outward along the station's normal, down to the contact point — fast then decelerating into contact, matching a real probe's slow final approach), 420-600ms dwell-at-contact (the trigger fires — a luminance-only flash: the probe tip's fill radius and a soft outer ring pulse via `Math.sin` peaking at the dwell midpoint, drawn twice at increasing alpha/radius in `--foreground`, never a colour change or `--ns-accent`), 600-900ms retract (ease-in-cubic back out along the same normal to the approach point, mirroring the approach's deceleration with an acceleration away from the part), 900-1200ms travel (ease-in-out-cubic straight-line interpolation from the current station's approach point to the next station's approach point, so both departure and arrival are visibly eased, never a blink-swap). A full lap is 18 x 1200ms = 21600ms and the station index simply increments mod 18 forever — there is no terminal state, no lap-complete pause.\n\nTouched-point age is computed analytically, with no history array: station k's most recent contact time within a lap is `k*1200 + 420`ms, so `age = ((elapsed mod 21600) - thatTime + 21600) mod 21600` is always exactly 'time since k was last touched', correct whether that touch happened earlier in the current lap or the previous one. Every station is drawn every frame in two passes, exactly mirroring the alpha-ramp convention used elsewhere in this registry: a base pass at full alpha in `--ns-muted` under all 18 stations, then an overlay pass in `--foreground` at alpha = `clamp01(1 - age/21600)` — near-opaque for a just-touched station, fully transparent (letting the muted base show through) for one about to roll off the trail's one-lap window. The probe tip itself is a small filled circle in `--foreground` with a thin trailing stylus-shaft stroke (60% alpha) drawn outward along the active station's current normal, so it reads as a stylus tip rather than a bare dot. The contour itself is stroked once per frame in `--border` (a separator token, never a fill) at roughly 0.6% of the geometry scale.\n\nGeometry scale is derived from the container's smaller dimension (`scale = min(width,height) * 0.42`), centred in the canvas, recomputed on every debounced ResizeObserver callback (which also repositions the 18 accessible overlay buttons). `--foreground`, `--ns-muted` and `--border` are read via `getComputedStyle(document.documentElement)` on mount and re-read on a `MutationObserver` watching `documentElement`'s class, with a synchronous re-render on change — no paint happens before that first read. The rAF loop is paused via `IntersectionObserver` (canvas out of view) and `document.hidden`, and resumed cleanly on either becoming true again, with `last` reset to 0 so the next frame's delta doesn't include the paused interval.\n\nInteraction: 18 zero-visual, `aria-label`-carrying `<button>` elements are absolutely positioned over each station (repositioned imperatively on resize, not on every animation frame, since station positions themselves never move) with a `focus-visible:ring-2 ring-ns-accent` focus ring — the one permitted accent use, interaction chrome only. Hover or focus shows a small `role=\"tooltip\"` div in `--ns-muted` text reading a synthetic per-station deviation value (a deterministic sine-hash of the station index, formatted to three decimal mm, stable across renders and identical under reduced motion). This overlay is pure DOM state (`activeStation`/`tooltipPos` via `useState`) layered on top of the canvas loop; it never reads from or writes to the virtual clock, so dwelling a pointer over a station cannot stall, speed up, or otherwise perturb the global 1200ms/station indexing cadence, the same isolation rule `peen-coverage`'s hover dwell follows for its own global pass clock.\n\nUnder `prefers-reduced-motion: reduce`, the rAF loop never starts; the virtual clock is set once, synchronously, to `21600 + 9*1200 + 420 + 90` (station index 9 — roughly opposite the lap's start — at the midpoint of its 180ms contact-dwell window, the single most legible frame: tip visibly touching, trigger flash mid-pulse, full one-lap trail history already resolved) and rendered once. That constant is exposed as `FREEZE_PHASE = \"contact-dwell-station9\"` and surfaced on the root element via `data-reduced-motion-freeze`. Cleanup on unmount cancels the rAF, disconnects the ResizeObserver, IntersectionObserver and MutationObserver, and clears the debounce timer. Zero dependencies, canvas + DOM overlay + CSS only, every colour a token (`--background` `--foreground` `--ns-muted` `--border` `--ns-accent`)."
  },
  "type": "registry:ui"
}