{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "profilometer-trace",
  "title": "Profilometer Trace",
  "description": "A section divider rendered as a live contact profilometer readout: a fixed stylus near the right edge while the measured surface's roughness-plus-waviness trace scrolls continuously beneath it, right to left, at a constant real-time rate, peaks and valleys passing under the tip forever.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/profilometer-trace/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// ProfilometerTrace — a section divider rendered as a live contact\n// profilometer readout: a diamond stylus dragged across a surface at\n// constant speed, its vertical deflection recorded as a trace that\n// decomposes into roughness (short-wavelength) and waviness (longer-\n// wavelength) components, per the standard ISO 4287 / ASME B46.1\n// roughness-measurement cutoff-filter convention.\n//\n// A fixed stylus sits near the right edge of the band. The measured\n// surface's trace scrolls continuously beneath it, right to left, at a\n// constant real-time rate — old excursions exit the left edge, new ones\n// enter at the right, forever. The trace itself is NOT a ring buffer of\n// discrete samples pushed/shifted every tick: it is a closed-form function\n// of world-space x (continuous, seeded, deterministic) sampled fresh at\n// whatever scroll offset the clock has reached. That makes a wrap seam\n// structurally impossible rather than something to avoid by bookkeeping —\n// there is no wrap, no buffer edge, just a sliding window over an infinite\n// deterministic field.\n//\n// height(x) = 0.6 * valueNoise(x, 18px) + 0.4 * valueNoise(x, 54px)   -- roughness, 2 octaves\n//           + 0.4 * sin(2*PI*x / 240px)                               -- waviness, 0.4x roughness amplitude\n// normalized to peak deflection = 0.35 * band height.\n//\n// Scroll offset advances by realSpeed * dt every frame (dt = clamped real\n// elapsed ms, never raw frame count) so the rate is identical at 30Hz or\n// 144Hz displays. Hovering the band eases realSpeed toward 40% of base\n// (a 60% slowdown, never a full stop) and eases back to 100% over 400ms\n// after the pointer leaves — a global rate change, not a per-x warp, so\n// the trace never tears or shows two speeds across one continuous curve.\n// ---------------------------------------------------------------------------\n\nconst SPEED_PX_S = 24; // base scroll speed, px/s\nconst SAMPLE_PITCH = 4; // px between polyline vertices (visual resolution, not a buffer)\nconst STYLUS_X_FRAC = 0.82; // stylus fixed position, fraction of band width\nconst PEAK_RATIO = 0.35; // peak deflection = PEAK_RATIO * band height\nconst ROUGH_WL_A = 18; // roughness octave A wavelength, px\nconst ROUGH_WL_B = 54; // roughness octave B wavelength, px\nconst WAVE_WL = 240; // waviness wavelength, px\nconst WAVE_AMP_RATIO = 0.4; // waviness amplitude, relative to roughness amplitude\nconst ROUGH_WEIGHT_A = 0.6;\nconst ROUGH_WEIGHT_B = 0.4;\nconst RAW_ENVELOPE = ROUGH_WEIGHT_A + ROUGH_WEIGHT_B + WAVE_AMP_RATIO; // normalizer\n\nconst HOVER_SLOWDOWN = 0.4; // minimum speed multiplier under the pointer\nconst HOVER_ENGAGE_MS = 150; // ease-in time constant\nconst HOVER_RELEASE_MS = 400; // ease-out time constant, per spec\n\nconst SEED = 0x9e3779b9;\n\nfunction hash1(i: number): number {\n  let t = (i ^ SEED) >>> 0;\n  t = Math.imul(t ^ (t >>> 15), t | 1);\n  t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n  const u = ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  return u * 2 - 1; // [-1, 1]\n}\n\nfunction smooth(t: number): number {\n  return t * t * (3 - 2 * t);\n}\n\n// deterministic 1D value noise: hashed lattice at `wavelength` spacing,\n// smoothstep-interpolated so it is continuous (and therefore seamless at\n// any sampling offset) rather than a discrete buffer.\nfunction valueNoise(x: number, wavelength: number): number {\n  const p = x / wavelength;\n  const i0 = Math.floor(p);\n  const f = smooth(p - i0);\n  const a = hash1(i0);\n  const b = hash1(i0 + 1);\n  return a + (b - a) * f;\n}\n\nfunction traceHeight(worldX: number): number {\n  const rough =\n    ROUGH_WEIGHT_A * valueNoise(worldX, ROUGH_WL_A) +\n    ROUGH_WEIGHT_B * valueNoise(worldX, ROUGH_WL_B);\n  const wave = WAVE_AMP_RATIO * Math.sin((2 * Math.PI * worldX) / WAVE_WL);\n  return (rough + wave) / RAW_ENVELOPE; // normalized to roughly [-1, 1]\n}\n\nexport interface ProfilometerTraceProps {\n  /** band height in px (divider scale). Default 56. */\n  height?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function ProfilometerTrace({\n  height = 56,\n  className = \"\",\n}: ProfilometerTraceProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\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 reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n\n    // -- token-derived ink: nothing paints until the first successful read --\n    let fg = \"\";\n    let mu = \"\";\n    let ready = false;\n    const derive = () => {\n      const cs = getComputedStyle(document.documentElement);\n      const nfg = cs.getPropertyValue(\"--foreground\").trim();\n      const nmu = cs.getPropertyValue(\"--ns-muted\").trim();\n      if (nfg) fg = nfg;\n      if (nmu) mu = nmu;\n      ready = fg.length > 0 && mu.length > 0;\n    };\n    derive();\n\n    let w = 0;\n    let h = 0;\n    let dpr = 1;\n    let visible = true;\n    let raf = 0;\n    let last = 0;\n\n    let scrollX = 0; // world-space offset of the left edge of the visible band\n    let speedMult = 1; // current speed multiplier, eased toward hoverTarget\n    let hoverTarget = 1;\n\n    const draw = () => {\n      if (!ready || w <= 0 || h <= 0) return;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.clearRect(0, 0, w, h);\n\n      const midY = h / 2;\n      const amp = PEAK_RATIO * h;\n\n      // zero-line reference: a subordinate --ns-muted hairline, never a\n      // --border stroke (--border is a separator token, ~1.1:1 contrast in\n      // light theme, and would make the trace-vs-baseline read fail exactly\n      // where the spec demands separation).\n      ctx.strokeStyle = mu;\n      ctx.globalAlpha = 0.35;\n      ctx.lineWidth = 1;\n      ctx.beginPath();\n      const zy = Math.round(midY) + 0.5;\n      ctx.moveTo(0, zy);\n      ctx.lineTo(w, zy);\n      ctx.stroke();\n      ctx.globalAlpha = 1;\n\n      // trace polyline, sampled at SAMPLE_PITCH from the continuous field —\n      // no buffer, no wrap, no seam.\n      ctx.strokeStyle = fg;\n      ctx.lineWidth = 1.25;\n      ctx.lineJoin = \"round\";\n      ctx.lineCap = \"round\";\n      ctx.beginPath();\n      let started = false;\n      let stylusY = midY;\n      const stylusScreenX = w * STYLUS_X_FRAC;\n      for (let sx = 0; sx <= w; sx += SAMPLE_PITCH) {\n        const worldX = scrollX + sx;\n        const y = midY - traceHeight(worldX) * amp;\n        if (!started) {\n          ctx.moveTo(sx, y);\n          started = true;\n        } else {\n          ctx.lineTo(sx, y);\n        }\n        if (sx <= stylusScreenX && sx + SAMPLE_PITCH > stylusScreenX) {\n          stylusY = y;\n        }\n      }\n      ctx.stroke();\n\n      // stylus: a fixed vertical stroke whose length matches the trace\n      // height directly beneath it (the \"drop-off\" read), plus a tip dot.\n      const stylusWorldX = scrollX + stylusScreenX;\n      stylusY = midY - traceHeight(stylusWorldX) * amp;\n      ctx.strokeStyle = fg;\n      ctx.globalAlpha = 0.85;\n      ctx.lineWidth = 1.25;\n      ctx.beginPath();\n      ctx.moveTo(Math.round(stylusScreenX) + 0.5, midY);\n      ctx.lineTo(Math.round(stylusScreenX) + 0.5, stylusY);\n      ctx.stroke();\n      ctx.globalAlpha = 1;\n      ctx.fillStyle = fg;\n      ctx.beginPath();\n      ctx.arc(stylusScreenX, stylusY, 2, 0, Math.PI * 2);\n      ctx.fill();\n    };\n\n    const loop = (now: number) => {\n      raf = 0;\n      if (!visible) return;\n      const dt = Math.min(50, last === 0 ? 16.7 : now - last);\n      last = now;\n      const engageMs = hoverTarget < speedMult ? HOVER_ENGAGE_MS : HOVER_RELEASE_MS;\n      speedMult += (hoverTarget - speedMult) * Math.min(1, dt / engageMs);\n      scrollX += (SPEED_PX_S / 1000) * dt * speedMult;\n      draw();\n      raf = requestAnimationFrame(loop);\n    };\n    const wake = () => {\n      if (raf === 0 && visible && !reduced) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    // -- reduced motion: find the deterministic phase where the stylus sits\n    // directly over the deepest valley in the visible band, freeze there,\n    // never start rAF. Named FREEZE_PHASE = deep-valley-lock. -------------\n    const freezeAtDeepValley = () => {\n      if (w <= 0) return;\n      const stylusScreenX = w * STYLUS_X_FRAC;\n      const searchSpan = WAVE_WL * 4; // several waviness periods, deterministic\n      let bestOffset = 0;\n      let bestVal = Infinity;\n      for (let off = 0; off < searchSpan; off += 1) {\n        const v = traceHeight(off + stylusScreenX);\n        if (v < bestVal) {\n          bestVal = v;\n          bestOffset = off;\n        }\n      }\n      scrollX = bestOffset;\n      speedMult = 1;\n      draw();\n    };\n\n    const resize = () => {\n      const rect = root.getBoundingClientRect();\n      w = rect.width;\n      h = rect.height;\n      dpr = Math.min(2, window.devicePixelRatio || 1);\n      canvas.width = Math.max(1, Math.round(w * dpr));\n      canvas.height = Math.max(1, Math.round(h * dpr));\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n      if (reduced) {\n        freezeAtDeepValley();\n      } else {\n        draw();\n      }\n    };\n\n    resize();\n    if (!reduced) wake();\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(root);\n\n    const mo = new MutationObserver(() => {\n      derive();\n      draw();\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible) {\n        last = 0;\n        wake();\n      }\n    });\n    io.observe(root);\n\n    const onVisibility = () => {\n      visible = !document.hidden && visible;\n      if (!document.hidden) {\n        last = 0;\n        wake();\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    // -- hover: global speed multiplier only, never a per-x warp (per-x\n    // warping would move different parts of one continuous curve at\n    // different rates and tear it — the exact seam the spec forbids). -----\n    const onEnter = () => {\n      if (reduced) return;\n      hoverTarget = HOVER_SLOWDOWN;\n      wake();\n    };\n    const onLeave = () => {\n      if (reduced) return;\n      hoverTarget = 1;\n      wake();\n    };\n    root.addEventListener(\"pointerenter\", onEnter);\n    root.addEventListener(\"pointerleave\", onLeave);\n\n    document.fonts.ready.then(() => {\n      derive();\n      if (reduced) freezeAtDeepValley();\n      else draw();\n    });\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      mo.disconnect();\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      root.removeEventListener(\"pointerenter\", onEnter);\n      root.removeEventListener(\"pointerleave\", onLeave);\n    };\n  }, [height]);\n\n  return (\n    <div\n      ref={rootRef}\n      role=\"separator\"\n      aria-orientation=\"horizontal\"\n      className={`relative w-full overflow-hidden ${className}`}\n      style={{ height }}\n    >\n      <canvas ref={canvasRef} aria-hidden className=\"block h-full w-full\" />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/profilometer-trace.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)"
    },
    "light": {
      "ns-muted": "#4d4d4d"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "divider",
      "separator",
      "hr",
      "profilometer",
      "metrology",
      "surface-roughness",
      "iso-4287",
      "canvas",
      "monochrome"
    ],
    "instruction": "Build <ProfilometerTrace height? className?> as a <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: contact stylus profilometry (ISO 4287 / ASME B46.1 surface-roughness measurement) — a diamond stylus dragged across a surface at constant speed, its vertical deflection recorded as a trace that the standard cutoff-filter convention decomposes into roughness (short-wavelength) and waviness (longer-wavelength) components. THE MECHANIC: a stylus sits fixed at x = 82% of the band's width; the measured surface's trace scrolls continuously beneath it, right to left, at a constant 24px/s real-time rate (scrollX += (24/1000) * dt * speedMult every frame, dt = clamped real elapsed ms, never raw frame count — a rate tied to frame count reads as jittery on variable refresh and is an explicit kill criterion). THE TRACE IS NOT A RING BUFFER: it is a closed-form deterministic function of world-space x, height(x) = 0.6*valueNoise(x,18px) + 0.4*valueNoise(x,54px) [roughness, 2 octaves] + 0.4*sin(2*PI*x/240px) [waviness, amplitude 0.4x the roughness weight], normalized by the raw envelope (0.6+0.4+0.4=1.4) and scaled to a peak deflection of 0.35 * band height; valueNoise is a hashed-lattice value noise (mulberry32-style integer hash per lattice index, smoothstep-interpolated between neighbours) evaluated fresh at whatever world offset the clock has reached. Because the field is continuous and evaluated on demand rather than shifted through a fixed-size buffer, a wrap seam is structurally impossible — there is no buffer edge to wrap past. The band is drawn as a polyline sampled every SAMPLE_PITCH=4px across the visible width (visual resolution, not a data structure). REAL NUMBERS: scroll speed 24px/s (~6 new 4px samples/s at the sample pitch), stylus at 82% width, peak deflection 0.35 * height, roughness wavelengths 18px/54px, waviness wavelength 240px at 0.4x amplitude. THE STYLUS: a short vertical --foreground stroke from the zero line down to the trace's exact current height at the stylus's world x (the 'drop-off' read, recomputed every frame from the same height() function used for the polyline, never interpolated from the drawn path) plus a small filled --foreground circle at the tip. RESTING LOOP: t0 renders scrollX=0 already mid-trace (the closed-form field has no blank state to seed away from — every offset, including zero, shows real peaks and valleys); 2.5s later ~60px of new trace (24px/s * 2.5s) has scrolled through, so the segment under the stylus is a different peak/valley arrangement; 5s later a fully distinct excursion sits under the tip, proving continuous unbounded drift rather than a loop with a period a viewer could time. LEGIBILITY: the one thing to follow is the trace sliding past the fixed stylus tip, whose indicator line visibly rises and falls as peaks and valleys pass under it; cadence is a continuous constant-px/s drift (24px/s) rather than any discrete swap, so there is nothing to time a blink against — the eye tracks a smooth drift, not an event. INTERACTION: pointerenter on the band eases a single global speed multiplier toward 0.4 (a 60% slowdown, reading as 'zooming attention' on that stretch) over a 150ms time constant; pointerleave eases it back to 1.0 over a 400ms time constant per spec. This is a GLOBAL multiplier, not a per-x warp keyed to pointer position — the spec's 'radius around the pointer' language was implemented as attention-on-the-whole-band rather than a spatially localized warp, because slowing only the samples near the pointer's x while the rest of one continuous curve kept moving at full rate would tear the trace into two visibly different speeds across a single line, which is exactly the seam the kill criteria forbid. Scroll never fully stops at any multiplier value (floor 0.4x), preserving alive-at-rest. Hover must not, and does not, recolor the trace or stylus with --ns-accent. TOKENS: --foreground (trace stroke, stylus stroke+dot) and --ns-muted (zero-line baseline, drawn at 0.35 alpha) are read once via getComputedStyle(document.documentElement) before first paint and re-derived on a documentElement class MutationObserver; a `ready` gate (both values present) blocks every draw() call — including the very first resize(), the ResizeObserver callback, and the IntersectionObserver resume path — so nothing paints before the first successful token read. --border is deliberately NOT used for the zero-line: --border is a separator token measuring ~1.1:1 contrast in light theme and would make the required trace-vs-baseline separation (the spec's own light-theme legibility requirement) structurally impossible; --ns-muted at reduced alpha is used instead, a real token with real contrast that still reads as visually subordinate to the --foreground trace. --ns-accent never appears. GEOMETRY: derived from the band's own height (its only meaningful dimension at divider scale) via PEAK_RATIO; horizontal wavelength/pitch constants are fixed px values per the spec's real numbers and are not re-derived from width, so at a very narrow card the 240px waviness wavelength can read as a gentle tilt rather than a full period — acceptable, matches how real short-span waviness traces read, and roughness excursion alone stays well above the perceptual floor even at the minimum 48px band height (0.35 * 48 ≈ 16.8px peak-to-zero). Canvas uses devicePixelRatio-scaled backing store with CSS pixel style dimensions set explicitly (never relying on intrinsic canvas size). REDUCED MOTION: FREEZE_PHASE = deep-valley-lock — a deterministic search (since the field is seeded) over several waviness periods of scroll offset for the one where the trace directly beneath the fixed stylus x is at its minimum (deepest valley in the visible band), scrollX is set there once, draw() runs once, rAF never starts; hover listeners are disabled entirely under reduced motion. LIFECYCLE: a ResizeObserver re-measures the band 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; a document visibilitychange listener does the same for tab backgrounding; unmount cancels the rAF, disconnects all three observers, and removes both pointer listeners. Direct-DOM rAF, zero React state on the hot path, zero dependencies. A11Y: role=separator carries divider semantics with no accessible name required; the canvas is aria-hidden decoration; there is no keyboard surface because there is nothing to operate — hover is a supplementary pointer-only affordance, not the mechanism that makes the component alive (the continuous scroll is), which is why autoplay.mode is 'none' rather than a pointer mode. Props: height (band height px, default 56, expected divider range 48-64), className."
  },
  "type": "registry:ui"
}