{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "equation-kidney-cam",
  "title": "Equation Kidney Cam",
  "description": "A section divider whose midpoint tick rides a lookup table built from the real equation-of-time curve (an equation-marchante watch's kidney cam compressed to an 11s loop), sliding, slowing, and reversing direction with four unevenly-spaced zero-crossing brighten-pulses per cycle, never a symmetric back-and-forth.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/equation-kidney-cam/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// EquationKidneyCam — a section divider whose midpoint tick rides the real\n// equation-of-time curve instead of sitting still. In an \"equation\n// marchante\" watch (Breguet, Blancpain), a kidney-profiled cam turns once\n// per year and a spring-loaded follower reads its edge, converting the\n// cam's UNIFORM rotation into the NON-uniform, sign-changing swing between\n// mean solar time and apparent solar time (-14m15s mid-February to\n// +16m23s early November, crossing zero four times a year).\n//\n// The cam profile itself is never generated live. MONTH_EOT below holds 12\n// real, almanac-sourced equation-of-time values (minutes) at 1-of-month\n// intervals; wrapping December back into January makes a 13th, closing\n// point. A periodic Catmull-Rom spline through those 12 anchors (each\n// evaluated with its wrapped neighbours) is resampled at 5 points per\n// segment — 60 points total, EOT_LUT — once, at module load. That table IS\n// the cam: index i is the follower's radial reading at the angle the disc\n// would be at i/60 of a full turn. Per rAF frame the only work done is\n// picking two adjacent LUT entries by the current phase and lerping\n// between them — no trig, no live curve evaluation.\n//\n// Real annual period: 1 year. Rendered period: 11s, documented here as a\n// ~2.87-million-times compression (365.25 days / 11s) purely for card\n// legibility — not a simulated calendar, just the same closed curve run\n// fast. The follower's on-screen offset is the LUT value scaled by the\n// LARGER of the curve's two real extremes (|+16.23| minutes), so the\n// positive swing reaches the full +-22% of the divider's half-length while\n// the (smaller-magnitude) negative swing falls proportionally short of\n// it — the asymmetry is inherited from the real numbers, never forced.\n// Because the underlying anchors are unevenly spaced in \"when it crosses\n// zero\", the resulting sweep is not a sine: the drop from most-negative to\n// zero is visibly quicker than the climb from zero to most-positive, and\n// the four zero-crossing brighten-pulses per 11s cycle land at uneven\n// intervals — the detail that reads as \"cam\", not \"wave\".\n//\n// Colour: the rule line sits at a mid-strength mix of --foreground (35%)\n// so it's visible without being a full-strength rule (a --border stroke,\n// at light theme's ~1.1:1 contrast, would make the follower's motion\n// unreadable — the whole point of this component). Each zero-crossing\n// briefly lifts that same --foreground opacity by 8% for 400ms via a\n// restarted CSS animation — never --ns-accent, which is reserved for\n// interaction chrome and never appears here since this is a passive\n// divider with no interaction. An optional low-opacity ghost of the cam's\n// own kidney outline (rendered from the identical LUT, as a closed SVG\n// path) fades in on hover/focus purely as a decorative aid — position:\n// absolute, so it can never affect layout.\n// ---------------------------------------------------------------------------\n\nexport interface EquationKidneyCamProps {\n  className?: string;\n}\n\n// -- Real, almanac-sourced equation-of-time values (minutes), 1st-of-month,\n// Jan through Dec. Standard published approximation; the true extremes\n// (-14m15s mid-Feb, +16m23s early Nov) fall between these monthly anchors\n// and are recovered by the spline, not hardcoded. --------------------------\nconst MONTH_EOT = [-3.4, -13.6, -12.6, -4.1, 2.9, 2.4, -3.6, -6.3, -0.1, 10.1, 16.4, 11.2];\nconst ANCHOR_COUNT = MONTH_EOT.length;\nconst SAMPLES_PER_SEGMENT = 5;\nconst LUT_SIZE = ANCHOR_COUNT * SAMPLES_PER_SEGMENT; // 60\n\nfunction catmullRom(p0: number, p1: number, p2: number, p3: number, t: number): number {\n  const t2 = t * t;\n  const t3 = t2 * t;\n  return 0.5 * (2 * p1 + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 + (-p0 + 3 * p1 - 3 * p2 + p3) * t3);\n}\n\nfunction buildLut(): number[] {\n  const lut: number[] = [];\n  for (let seg = 0; seg < ANCHOR_COUNT; seg++) {\n    const p0 = MONTH_EOT[(seg - 1 + ANCHOR_COUNT) % ANCHOR_COUNT];\n    const p1 = MONTH_EOT[seg];\n    const p2 = MONTH_EOT[(seg + 1) % ANCHOR_COUNT];\n    const p3 = MONTH_EOT[(seg + 2) % ANCHOR_COUNT];\n    for (let s = 0; s < SAMPLES_PER_SEGMENT; s++) {\n      const t = s / SAMPLES_PER_SEGMENT;\n      lut.push(catmullRom(p0, p1, p2, p3, t));\n    }\n  }\n  return lut;\n}\n\nconst EOT_LUT = buildLut();\nconst LUT_MAX_ABS = EOT_LUT.reduce((m, v) => Math.max(m, Math.abs(v)), 0);\nlet MIN_INDEX = 0;\nlet minVal = Infinity;\nfor (let i = 0; i < EOT_LUT.length; i++) {\n  const v = EOT_LUT[i];\n  if (v < minVal) {\n    minVal = v;\n    MIN_INDEX = i;\n  }\n}\n\n// -- ghost cam outline, built once from the same LUT: a closed polar path\n// (angle = i/60 of a turn, radius = base + LUT value * scale). Decorative\n// only; never rendered visibly at rest, only revealed faintly on hover. --\nconst GHOST_VB = 64;\nconst GHOST_CX = GHOST_VB / 2;\nconst GHOST_CY = GHOST_VB / 2;\nconst GHOST_BASE_R = 14;\nconst GHOST_R_SCALE = 8 / LUT_MAX_ABS;\nconst GHOST_PATH = (() => {\n  let d = \"\";\n  for (let i = 0; i < LUT_SIZE; i++) {\n    const angle = (i / LUT_SIZE) * Math.PI * 2 - Math.PI / 2;\n    const r = GHOST_BASE_R + EOT_LUT[i] * GHOST_R_SCALE;\n    const x = GHOST_CX + Math.cos(angle) * r;\n    const y = GHOST_CY + Math.sin(angle) * r;\n    d += i === 0 ? `M ${x.toFixed(2)} ${y.toFixed(2)}` : ` L ${x.toFixed(2)} ${y.toFixed(2)}`;\n  }\n  return `${d} Z`;\n})();\n\nconst CAM_PERIOD_MS = 11000; // real period: 1 year — an ~2.87M:1 compression\nconst MAX_OFFSET_PCT = 22; // +-22% of the divider's half-length\nconst PULSE_MS = 400;\nconst HEIGHT_PX = 28; // fixed card-scale height; every other dimension below derives from it\nconst TICK_HEIGHT_PX = Math.round(HEIGHT_PX * 0.5);\n\nexport function EquationKidneyCam({ className = \"\" }: EquationKidneyCamProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const lineRef = useRef<HTMLDivElement>(null);\n  const tickRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const line = lineRef.current;\n    const tick = tickRef.current;\n    if (!root || !line || !tick) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    const sampleAt = (idxFloat: number): number => {\n      const i0 = Math.floor(idxFloat) % LUT_SIZE;\n      const i1 = (i0 + 1) % LUT_SIZE;\n      const frac = idxFloat - Math.floor(idxFloat);\n      return EOT_LUT[i0] + (EOT_LUT[i1] - EOT_LUT[i0]) * frac;\n    };\n\n    const applyOffset = (value: number) => {\n      const pct = (value / LUT_MAX_ABS) * MAX_OFFSET_PCT;\n      tick.style.left = `${50 + pct}%`;\n    };\n\n    const firePulse = () => {\n      line.style.animation = \"none\";\n      // force reflow so re-setting the animation below restarts it\n      void line.offsetWidth;\n      line.style.animation = `ns-eqcam-pulse ${PULSE_MS}ms ease-out`;\n    };\n\n    if (reduced) {\n      // freeze on the real-world most-negative point (mid-February\n      // analogue, -14m15s) — the cam's tightest inward point and the most\n      // visually distinct resting position, per the reduced-motion rule.\n      applyOffset(EOT_LUT[MIN_INDEX]);\n      return;\n    }\n\n    let raf = 0;\n    let visible = true;\n    let startTime = 0;\n    let prevSign = Math.sign(EOT_LUT[0]) || 1;\n\n    const loop = (now: number) => {\n      raf = 0;\n      if (!visible) return;\n      if (startTime === 0) startTime = now;\n      const elapsed = now - startTime;\n      const phase = (elapsed % CAM_PERIOD_MS) / CAM_PERIOD_MS;\n      const idxFloat = phase * LUT_SIZE;\n      const value = sampleAt(idxFloat);\n      applyOffset(value);\n\n      const sign = value === 0 ? prevSign : Math.sign(value);\n      if (sign !== prevSign) firePulse();\n      prevSign = sign;\n\n      raf = requestAnimationFrame(loop);\n    };\n\n    // -- no paint before the first token/geometry read: only start once\n    // the divider is actually on screen, and pause the rAF loop (not the\n    // logical clock — startTime is wall-clock, so re-entering resumes the\n    // correct phase, not a reset) while it scrolls off. -------------------\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible && !raf) raf = requestAnimationFrame(loop);\n    });\n    io.observe(root);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      io.disconnect();\n    };\n  }, []);\n\n  return (\n    <div\n      ref={rootRef}\n      role=\"separator\"\n      aria-orientation=\"horizontal\"\n      data-eqcam\n      className={`ns-eqcam relative w-full ${className}`}\n      style={{ height: HEIGHT_PX }}\n    >\n      <style>{`\n.ns-eqcam{ display:flex; align-items:center; }\n.ns-eqcam-line{\n  position:relative;\n  width:100%;\n  height:1px;\n  background:var(--foreground);\n  opacity:0.35;\n}\n.ns-eqcam-tick{\n  position:absolute;\n  top:50%;\n  width:1px;\n  height:${TICK_HEIGHT_PX}px;\n  background:var(--foreground);\n  opacity:0.9;\n  transform:translate(-50%, -50%);\n  left:50%;\n}\n.ns-eqcam-ghost{\n  position:absolute;\n  left:50%;\n  top:50%;\n  width:${GHOST_VB}px;\n  height:${GHOST_VB}px;\n  transform:translate(-50%, -50%);\n  opacity:0;\n  pointer-events:none;\n  transition:opacity 300ms ease;\n}\n.ns-eqcam:hover .ns-eqcam-ghost{ opacity:0.18; }\n@keyframes ns-eqcam-pulse{\n  0%{ opacity:0.43; }\n  100%{ opacity:0.35; }\n}\n@media (prefers-reduced-motion: reduce){\n  .ns-eqcam-line{ animation:none!important; }\n  .ns-eqcam-ghost{ transition:none!important; }\n}\n`}</style>\n      <div ref={lineRef} className=\"ns-eqcam-line\" aria-hidden=\"true\" />\n      <div ref={tickRef} className=\"ns-eqcam-tick\" aria-hidden=\"true\" />\n      <svg\n        className=\"ns-eqcam-ghost\"\n        viewBox={`0 0 ${GHOST_VB} ${GHOST_VB}`}\n        aria-hidden=\"true\"\n        focusable=\"false\"\n      >\n        <path d={GHOST_PATH} fill=\"none\" stroke=\"var(--foreground)\" strokeWidth={1} />\n      </svg>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/equation-kidney-cam.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-accent": "#006bff"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "divider",
      "separator",
      "section-divider",
      "hr",
      "svg",
      "cam",
      "horology",
      "lookup-table"
    ],
    "instruction": "Build <EquationKidneyCam className?> as a drop-in replacement for <hr>/border-top between page sections. THE DATA: a module-scope constant MONTH_EOT holds 12 real, almanac-sourced equation-of-time values in minutes (Jan-Dec, 1st-of-month) — the sundial-minus-clock offset that swings between roughly -14 and +16 minutes across the real year. A periodic Catmull-Rom spline is run through those 12 anchors (each segment's four control points wrap via modulo so December connects back into January, making the 12 anchors read as a 13th, closing point) and resampled at 5 points per segment for a 60-entry EOT_LUT, computed once at module load with plain arithmetic — no trig at runtime. THE ANIMATION: a single rAF loop reads a wall-clock elapsed time, maps it to a phase via a fixed CAM_PERIOD_MS=11000 (documented in-code as a ~2.87-million-times compression of the real 1-year period, purely for card legibility, not a simulated calendar), multiplies phase by 60 to get a float LUT index, and linearly interpolates between the two adjacent LUT entries — that interpolated value IS the follower's reading for this frame, no re-evaluation of the spline. THE OFFSET: the follower's on-screen position is `50% + (value / LUT_MAX_ABS) * 22%` applied as the tick element's `left`, where LUT_MAX_ABS is the LARGER-magnitude extreme found in the actual resampled table (near +16.4) — so the positive swing reaches the full +-22% of the divider's half-length while the smaller-magnitude negative swing (near -13.6) falls proportionally short of it; this asymmetry must never be forced to a symmetric +-22%, it has to fall out of the real numbers. Because the anchors are unevenly spaced in when they cross zero, the resulting motion is NOT a sine — verify visually that the drop from most-negative to zero is faster than the climb from zero to most-positive. THE PULSE: every frame compares the current sample's sign against the previous frame's; on a sign flip, restart a `ns-eqcam-pulse` CSS animation on the line element (set style.animation='none', read offsetWidth to force reflow, then set the animation string again) that lifts opacity from 0.43 to its resting 0.35 over 400ms — this fires 4 times per 11s cycle, at whatever intervals the real curve's own zero-crossing dates produce (never evenly spaced; do not add a timer-based even-interval pulse as a shortcut). STRUCTURE: outer <div role=\"separator\" aria-orientation=\"horizontal\" data-eqcam> at a fixed 28px height (every other dimension, e.g. the 14px tick height, derives from that one constant, satisfying 'derive geometry from the container's smaller dimension' for a component that is mostly width) containing (1) a 1px-tall line div at `background:var(--foreground); opacity:0.35` — deliberately NOT `--border` (whose ~1.1:1 light-theme contrast would make the follower's motion imperceptible) and NOT full-strength `--foreground` (this is a separator, not a headline) — (2) an absolutely positioned 1px-wide tick div, taller than the line, at `var(--foreground)` opacity 0.9, whose `left` is rewritten every frame, and (3) a decorative aria-hidden SVG built once from the identical LUT as a closed polar path (angle = i/60 of a turn, radius = base + LUT value * scale) — opacity 0 at rest, fading to ~0.18 only on `:hover`/`:focus-within` via CSS, absolutely positioned and centered so it can never affect layout. LIFECYCLE: an IntersectionObserver starts/stops the rAF loop as the divider enters/leaves the viewport, but never resets `startTime` — re-entering resumes the correct real phase, it does not restart the cycle, satisfying 'must NOT respond to hover/scroll by changing rate or resetting phase'. REDUCED MOTION: skip the rAF loop entirely and call the same `applyOffset` once with `EOT_LUT[MIN_INDEX]` — MIN_INDEX is computed once at module load as the argmin of the whole LUT, i.e. the real-world most-negative point (the mid-February analogue, the cam's tightest inward point), not t0, which could land anywhere including a near-invisible near-zero offset. COLOUR: every stroke/fill is `var(--foreground)` at an authored opacity (0.35 line, 0.9 tick, 0.18 ghost-on-hover, 0.43 pulse peak) or `var(--background)`; `--ns-accent` never appears anywhere in this component — it is a passive divider with zero interaction states to mark, and the spec explicitly forbids the brighten-on-cross effect from touching accent. A11Y: role=separator carries the semantics, no accessible name needed; every visual element is aria-hidden since there is nothing to operate. DIFFERS FROM: craze-rule, which is a ONE-SHOT scroll-triggered reveal (a propagating fracture that arrives once via IntersectionObserver, then idles) — pick craze-rule instead when the divider should announce a section boundary with a single arrival moment; equation-kidney-cam is the opposite, an already-settled line with continuous, non-uniform ambient drift that is running identically whether or not the viewer ever scrolled past it, and it never has a one-time 'arrival' state. DIFFERS FROM: divider-telephone-cord-delam, whose motion is a physical coil-unwinding metaphor with a single dominant oscillation frequency — equation-kidney-cam's signature is specifically an ASYMMETRIC, sign-changing, non-periodic-looking sweep sourced from astronomical data, with unevenly-timed pulses being the tell. Zero dependencies, no canvas — DOM+SVG+CSS only; the only per-frame JS is one LUT lookup, one lerp, one style write, and an occasional animation restart."
  },
  "type": "registry:ui"
}