{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cylinder-hillndale",
  "title": "Cylinder Hill-and-Dale",
  "description": "A full-width section divider rendered as a side elevation of an Edison-style wax cylinder phonograph: a rotating cylinder carrying a static helical hill-and-dale groove, a lead-screw rail, and a stylus carriage the lead-screw advances axially in exact lockstep with rotation, one wrap of travel per one full turn, always, at any frame rate.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/cylinder-hillndale/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// CylinderHillndale — a full-width section divider rendered as a side\n// elevation of an Edison-style cylinder phonograph: a rotating wax cylinder\n// carrying a helical \"hill-and-dale\" groove (depth-modulated, not the later\n// lateral-cut vinyl groove), a lead-screw rail above it, and a stylus\n// carriage that the lead-screw advances axially in EXACT lockstep with the\n// cylinder's rotation — one wrap of axial travel per one full turn, always,\n// at any frame rate. That lockstep is the entire mechanic: the carriage's\n// horizontal creep along the rail and the stylus arm's vertical bob (which\n// tracks the groove's hill/dale height at the carriage's own position) are\n// both driven off the SAME accumulated rotation angle, so the two motions\n// can never drift apart.\n//\n// Discriminator against loader-thread-spool (this repo's nearest neighbour\n// in \"something winds/turns\"): that component is a top-down concentric\n// coil winding radially outward, a spiral. This is a side elevation of a\n// horizontal cylinder — a straight helix wrapping a fixed-diameter barrel,\n// axial translation locked to rotation, no radial growth, no spool. The\n// groove pattern itself never animates; only the carriage/stylus and the\n// end-cap rotation tick move.\n// ---------------------------------------------------------------------------\n\nconst ROTATION_PERIOD_S = 2.6; // rendered seconds per cylinder revolution\nconst NUM_WRAPS = 10; // wrap crossings across one full traversal\nconst TRAVERSAL_S = NUM_WRAPS * ROTATION_PERIOD_S; // 26s, matches the real Edison 4\" cylinder pass\n\nconst END_CAP_RX_FACTOR = 0.22; // end-cap ellipse half-width, relative to radius\nconst AMPL_FACTOR = 0.62; // groove sine amplitude, relative to radius\nconst RAIL_GAP_FACTOR = 0.85; // lead-screw rail height above cylinder center, relative to radius\n\n// baked \"hill-and-dale\" depth envelope — a fixed spatial pattern cut into\n// the groove, three non-commensurate components so it never repeats\n// visibly across a single traversal. A function of AXIAL POSITION only,\n// never of time: the groove doesn't re-record itself, the carriage just\n// rides over whatever depth was already cut there.\nfunction grooveDepth(xFrac: number) {\n  const a =\n    0.5 +\n    0.5 *\n      Math.sin(2 * Math.PI * 3.1 * xFrac) *\n      Math.cos(2 * Math.PI * 1.7 * xFrac + 0.6);\n  const b = 0.5 + 0.5 * Math.sin(2 * Math.PI * 7.3 * xFrac + 2.1);\n  return Math.max(0, Math.min(1, 0.65 * a + 0.35 * b));\n}\n\nexport interface CylinderHillndaleProps {\n  /** band height in px; the cylinder's radius derives from this. Default 88. */\n  height?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function CylinderHillndale({\n  height = 88,\n  className = \"\",\n}: CylinderHillndaleProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let fg = \"currentColor\";\n    let border = \"currentColor\";\n    const readTokens = () => {\n      const root = getComputedStyle(document.documentElement);\n      fg = root.getPropertyValue(\"--foreground\").trim() || \"currentColor\";\n      border = root.getPropertyValue(\"--border\").trim() || \"currentColor\";\n    };\n\n    let width = 0;\n    let dpr = 1;\n    let sized = false;\n\n    // layout, recomputed on resize\n    let cx = { left: 0, right: 0 }; // cylinder body x-extent\n    let cy = 0; // vertical center of the cylinder\n    let radius = 0;\n    let railY = 0;\n    let wrapSpacing = 1;\n    let ampl = 1;\n\n    const layout = () => {\n      const rect = canvas.getBoundingClientRect();\n      width = rect.width;\n      if (width < 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\n      radius = height * 0.32;\n      const endCapRx = radius * END_CAP_RX_FACTOR;\n      const inset = endCapRx + radius * 0.15;\n      cx = { left: inset, right: width - inset };\n      cy = height * 0.58; // slightly below center so the rail above has room\n      railY = cy - radius * RAIL_GAP_FACTOR - radius * 0.55;\n      ampl = radius * AMPL_FACTOR;\n      const bodyLen = Math.max(1, cx.right - cx.left);\n      wrapSpacing = bodyLen / NUM_WRAPS;\n\n      sized = true;\n    };\n\n    const grooveY = (x: number) => {\n      const frac = (x - cx.left) / Math.max(1, cx.right - cx.left);\n      return cy + ampl * Math.sin((2 * Math.PI * (x - cx.left)) / wrapSpacing) * (0.75 + 0.25 * grooveDepth(frac));\n    };\n\n    const draw = (elapsedS: number) => {\n      if (!sized) return;\n      ctx.clearRect(0, 0, width, height);\n\n      const bodyLen = cx.right - cx.left;\n      const endCapRx = radius * END_CAP_RX_FACTOR;\n\n      // -- cylinder body outline -------------------------------------------\n      ctx.strokeStyle = fg;\n      ctx.lineWidth = 1.25;\n      ctx.beginPath();\n      ctx.moveTo(cx.left, cy - radius);\n      ctx.lineTo(cx.right, cy - radius);\n      ctx.moveTo(cx.left, cy + radius);\n      ctx.lineTo(cx.right, cy + radius);\n      ctx.stroke();\n\n      // rotation phase, unbounded — everything below derives from this\n      const revolutions = elapsedS / ROTATION_PERIOD_S;\n      const rotationAngle = revolutions * 2 * Math.PI;\n      const traversalFrac = (elapsedS / TRAVERSAL_S) % 1;\n      const carriageX = cx.left + traversalFrac * bodyLen;\n\n      // -- end caps, each with a rotation tick ------------------------------\n      for (const ex of [cx.left, cx.right]) {\n        ctx.strokeStyle = fg;\n        ctx.lineWidth = 1;\n        ctx.beginPath();\n        ctx.ellipse(ex, cy, endCapRx, radius, 0, 0, Math.PI * 2);\n        ctx.stroke();\n\n        // tick marking cylinder rotational phase — the \"barrel is turning\"\n        // cue, kept visually separate from the carriage's own motion\n        const tickAngle = rotationAngle % (2 * Math.PI);\n        const tx = ex + endCapRx * 0.72 * Math.sin(tickAngle);\n        const ty = cy + radius * 0.72 * Math.cos(tickAngle);\n        ctx.beginPath();\n        ctx.arc(tx, ty, 1.4, 0, Math.PI * 2);\n        ctx.fillStyle = fg;\n        ctx.fill();\n      }\n\n      // -- helical groove, static in space, depth-modulated stroke width --\n      // drawn as short segments so lineWidth can vary along its length —\n      // width encodes hill/dale depth, never a color or alpha shift\n      const STEP = Math.max(2, wrapSpacing / 14);\n      ctx.strokeStyle = fg;\n      let prevX = cx.left;\n      let prevY = grooveY(cx.left);\n      for (let x = cx.left + STEP; x <= cx.right + STEP; x += STEP) {\n        const clampedX = Math.min(x, cx.right);\n        const yv = grooveY(clampedX);\n        const frac = (clampedX - cx.left) / bodyLen;\n        ctx.lineWidth = 0.8 + grooveDepth(frac) * 1.8;\n        ctx.beginPath();\n        ctx.moveTo(prevX, prevY);\n        ctx.lineTo(clampedX, yv);\n        ctx.stroke();\n        prevX = clampedX;\n        prevY = yv;\n        if (clampedX >= cx.right) break;\n      }\n\n      // -- lead-screw rail: a true separator, never a fill -----------------\n      ctx.strokeStyle = border;\n      ctx.lineWidth = 1;\n      ctx.beginPath();\n      ctx.moveTo(cx.left, railY);\n      ctx.lineTo(cx.right, railY);\n      ctx.stroke();\n\n      // -- carriage: pure horizontal translation along the rail ------------\n      const carriageW = Math.max(6, radius * 0.36);\n      const carriageH = Math.max(4, radius * 0.24);\n      ctx.fillStyle = fg;\n      ctx.fillRect(\n        carriageX - carriageW / 2,\n        railY - carriageH / 2,\n        carriageW,\n        carriageH\n      );\n\n      // -- stylus arm: vertical link from carriage down to the groove ------\n      // its length is exactly the groove's local height under the current\n      // rotation phase — the one visible proof rotation and translation\n      // are locked, not two independent animations\n      const stylusY = grooveY(carriageX);\n      ctx.strokeStyle = fg;\n      ctx.lineWidth = 1.1;\n      ctx.beginPath();\n      ctx.moveTo(carriageX, railY + carriageH / 2);\n      ctx.lineTo(carriageX, stylusY);\n      ctx.stroke();\n      ctx.beginPath();\n      ctx.arc(carriageX, stylusY, 1.8, 0, Math.PI * 2);\n      ctx.fill();\n    };\n\n    // -- loop ----------------------------------------------------------------\n    let raf = 0;\n    let clockS = 0; // accumulated elapsed seconds, pauses cleanly on hide\n    let lastTs = 0;\n\n    const loop = (now: number) => {\n      const dt = lastTs ? Math.min(0.25, (now - lastTs) / 1000) : 1 / 60;\n      lastTs = now;\n      clockS += dt;\n      draw(clockS);\n      if (!document.hidden) raf = requestAnimationFrame(loop);\n    };\n\n    const start = () => {\n      cancelAnimationFrame(raf);\n      lastTs = 0;\n      raf = requestAnimationFrame(loop);\n    };\n\n    const mo = new MutationObserver(() => {\n      readTokens();\n      if (reduced) draw(TRAVERSAL_S * 0.4);\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    const ro = new ResizeObserver(() => {\n      layout();\n      if (reduced) draw(TRAVERSAL_S * 0.4);\n      else if (sized && !document.hidden) start();\n    });\n    ro.observe(canvas);\n\n    let io: IntersectionObserver | null = null;\n    if (!reduced) {\n      io = new IntersectionObserver(\n        (entries) => {\n          const visible = entries[0]?.isIntersecting;\n          if (visible && sized && !document.hidden) {\n            start();\n          } else {\n            cancelAnimationFrame(raf);\n          }\n        },\n        { threshold: 0.01 }\n      );\n      io.observe(canvas);\n    }\n\n    const onVis = () => {\n      cancelAnimationFrame(raf);\n      if (!document.hidden && !reduced && sized) start();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    document.fonts.ready.then(() => {\n      readTokens();\n      layout();\n      if (!sized) return;\n\n      if (reduced) {\n        // deliberately non-t0, most-structured frame: traversal progress\n        // 40% — the carriage is clearly past the start and the groove's\n        // depth-modulated stroke width is visibly non-uniform around it\n        draw(TRAVERSAL_S * 0.4);\n        return;\n      }\n\n      draw(0);\n      start();\n    });\n\n    return () => {\n      cancelAnimationFrame(raf);\n      mo.disconnect();\n      ro.disconnect();\n      io?.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, [height]);\n\n  return (\n    <div\n      role=\"separator\"\n      aria-orientation=\"horizontal\"\n      className={`ns-cyl w-full ${className}`}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden=\"true\"\n        className=\"block w-full text-foreground\"\n        style={{ height }}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/cylinder-hillndale.tsx"
    }
  ],
  "meta": {
    "collection": "core",
    "tags": [
      "divider",
      "separator",
      "hr",
      "phonograph",
      "cylinder",
      "edison",
      "helix",
      "lead-screw",
      "canvas",
      "monochrome"
    ],
    "instruction": "Build <CylinderHillndale height? className?> as a full-width <canvas> band wrapped in a <div role=\"separator\" aria-orientation=\"horizontal\">, a drop-in replacement for <hr>/border-top between page sections. SOURCE, NOT INVENTED: early Edison cylinder phonographs recorded via a vertically-modulated (\"hill-and-dale\") helical groove cut into a rotating wax cylinder — the stylus rides groove DEPTH variation, not the side-to-side lateral modulation Berliner's later disc records used. A lead-screw advances the stylus carriage axially at a fixed pitch in exact lockstep with cylinder rotation, which is what physically produces one continuous helix rather than concentric rings. THE MECHANIC: a cylinder turns while a stylus carriage rides its helical groove, stepping sideways by exactly one wrap's width for every full turn — rotation and translation locked together, never independent. GEOMETRY: side elevation only. radius = height * 0.32; end-cap ellipses at each end (rx = radius * 0.22, ry = radius); the cylinder body's horizontal extent (its two long edge lines at cy ± radius) spans from just past the left end-cap to just before the right, and that span IS the full canvas width scaled by the container — 'cylinder length mapped proportionally to canvas width' per spec. wrapSpacing = bodyLength / NUM_WRAPS where NUM_WRAPS is fixed at 10 — this is what keeps the two documented real numbers (a 6px wrap pitch and a 26s full traversal) mutually consistent at any rendered width: wrapSpacing scales with the container so the ratio bodyLength/wrapSpacing (and therefore traversal duration) never drifts, while ROTATION_PERIOD_S stays a fixed real time constant (2.6s per revolution, decoupled from Edison's real 160rpm) that never scales with size. THE NON-NEGOTIABLE IDENTITY: carriageX derives from rotationAngle, never from an independently-integrated position — traversalFrac = (elapsedSeconds / (NUM_WRAPS * ROTATION_PERIOD_S)) % 1; carriageX = bodyLeft + traversalFrac * bodyLength. Because this is computed directly from accumulated elapsed time every frame (not incremented per-frame and summed), the relationship 'one full wrap of translation exactly matches one full revolution' holds exactly regardless of frame rate or dropped frames — there is no drift to accumulate. THE GROOVE is a STATIC spatial curve, never animated over time: grooveY(x) = cy + amplitude * sin(2π * (x - bodyLeft) / wrapSpacing), amplitude = radius * 0.62, drawn once per frame across the whole body length as a sequence of short strokes (STEP ≈ wrapSpacing / 14) so each segment's lineWidth can independently encode a baked hill-and-dale DEPTH envelope — grooveDepth(xFraction), a fixed sum of two non-commensurate sine components (frequencies 3.1 and 7.3 cycles across the span, phase-offset) normalized 0..1 and mapped to lineWidth = 0.8 + depth * 1.8px. Depth is a function of axial position only, never of time or rotation phase — the groove doesn't re-record itself, the carriage just rides over whatever was already cut there, which is why the 'groove-depth shading pattern under it' visibly changes as the carriage's x advances, not as the clock ticks in place. STYLUS: the carriage itself is a small filled rect that translates PURELY HORIZONTALLY along a lead-screw rail line drawn above the cylinder body (at cy - radius * 1.4, stroked in --border as a true separator, never a fill); a thin --foreground stylus arm drops from the carriage down to grooveY(carriageX) — that arm's LENGTH is the one visible proof the two motions are locked, since it is computed from the exact same carriageX the carriage block itself uses, not a second independent oscillator. END CAPS: each end-cap ellipse carries a small filled tick at radius * 0.72 from center, angle = (elapsedSeconds / ROTATION_PERIOD_S * 2π) mod 2π — the explicit 'the barrel itself is turning' cue, kept visually and computationally separate from the carriage's translation so a viewer can confirm rotation is happening even while staring only at the carriage. RESTING LOOP: t0 is some arbitrary carriage axial position and rotational tick angle; at 2.5s (2.5s / 2.6s-per-rev ≈ one wrap crossing has just completed) the carriage sits roughly one full wrapSpacing further along the rail, a visibly different axial position, with a different groove-depth pattern under the stylus tip; at 5s the carriage has advanced roughly two wraps further still, clearly past its t0 position. Full traversal (NUM_WRAPS * ROTATION_PERIOD_S = 26s) wraps via modulo, producing the carriage's instant snap back to bodyLeft with a fresh pass beginning — an unbounded loop, never a process that finishes and stops. TOKENS: fg reads --foreground and border reads --border, both via getComputedStyle(document.documentElement), re-derived on a documentElement class MutationObserver so a theme flip is live; --ns-accent never appears anywhere — there is no interactive surface on a pure ambient divider, so it is never reserved for anything. --border is used exactly once, for the lead-screw rail line, and strictly as a separator stroke, never as a fill or the groove's own ink (the groove and cylinder outline are --foreground only, since --border measures ~1.1:1 in light theme and would make the groove's depth modulation structurally invisible). A ResizeObserver on the canvas re-lays-out and restarts the loop on container size changes; an IntersectionObserver pauses the rAF loop while off-screen and resumes it with a fresh last-timestamp (no giant delta-time jump) on re-entry. The loop also pauses on document.hidden and resumes cleanly on visibilitychange. prefers-reduced-motion skips the rAF loop entirely and draws exactly one frame at elapsedSeconds = TRAVERSAL_S * 0.4 — the carriage clearly past the start (not an ambiguous 'just began' t0 frame) with the groove's depth-modulated stroke width visibly non-uniform under and near the stylus tip. Direct-DOM rAF, zero React state on the hot path, zero dependencies. A11Y: role=separator carries the divider's semantics with no accessible name required; the canvas is aria-hidden decoration; there is no keyboard surface because there is nothing to operate, so the registry's tab-reachability check is correctly skipped. Props: height (band height px, default 88; radius and every other proportion derive from it), className."
  },
  "type": "registry:ui"
}