{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-story-strata",
  "title": "Scroll Story Strata",
  "description": "Pinned scroll story: the viewport becomes a geological core cross-section and scroll drives a drill bit descending through procedurally banded strata, with content panels crossfading per band and a mono HUD ticking depth.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/scroll-story-strata/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// CoreSampleScroll — pinned scroll story. The viewport becomes a geological\n// core cross-section: a 250vh track wraps a sticky 100vh stage, and scroll\n// drives a drill bit descending through procedurally banded strata while\n// content panels crossfade per band and a mono HUD ticks depth in meters.\n// Strata grain (2-octave value noise, 3px cells, quantized to 3 gray levels\n// between --border and --ns-muted, per-band thresholds) is rendered ONCE per\n// resize/theme into an offscreen band atlas and blitted per frame. Exactly\n// one accent vein — a 1.5px --ns-accent polyline at a fixed depth in band 4.\n// Direct-DOM rAF, refs only on the hot path; the loop sleeps when the eased\n// depth is within epsilon of target and the chip spray is empty.\n// ---------------------------------------------------------------------------\n\nexport interface CoreSampleBand {\n  id: string;\n  label: string;\n  title: string;\n  body: string;\n  spec: string;\n}\n\nconst DEFAULT_BANDS: CoreSampleBand[] = [\n  {\n    id: \"interface\",\n    label: \"Interface\",\n    title: \"Surface layer\",\n    body: \"Everything the user touches renders here: design tokens, optimistic state, and a 60fps interaction budget. Nothing below this band is visible, but every band below shapes how it feels.\",\n    spec: \"react 19 · edge-rendered · p75 INP 84ms\",\n  },\n  {\n    id: \"api\",\n    label: \"API\",\n    title: \"Contract stratum\",\n    body: \"A thin, typed boundary where every request is validated, shaped, and signed. Breaking changes surface here as schema drift long before they reach a client.\",\n    spec: \"graphql + rest · 214 endpoints · p99 121ms\",\n  },\n  {\n    id: \"engine\",\n    label: \"Engine\",\n    title: \"Compute band\",\n    body: \"Schedulers, queues, and the rules engine burn through about two million jobs a day. Backpressure is absorbed inside this band, so spikes never reach the surface.\",\n    spec: \"12 workers · 2.1M jobs/day · retry ×3\",\n  },\n  {\n    id: \"storage\",\n    label: \"Storage\",\n    title: \"Sedimented state\",\n    body: \"Every event since day zero is compacted into columnar layers — cheap to scan, impossible to lose. The accent vein marks the write-ahead log, the one seam everything above depends on.\",\n    spec: \"wal + columnar · 41TB · rpo 0s\",\n  },\n  {\n    id: \"bedrock\",\n    label: \"Bedrock\",\n    title: \"Bedrock / infra\",\n    body: \"Machines, networks, and the failure domains everything above stands on. It moves rarely, and when it does, the whole column feels it.\",\n    spec: \"3 regions · 9 AZs · chaos-tested weekly\",\n  },\n];\n\n// band thickness fractions — each within 12–28% of stage height, sum = 1\nconst FRACTIONS_5 = [0.17, 0.21, 0.15, 0.27, 0.2];\n\nfunction bandFractions(n: number): number[] {\n  if (n === 5) return FRACTIONS_5;\n  return new Array(n).fill(1 / Math.max(1, n));\n}\n\ntype Vec3 = [number, number, number];\n\nfunction parseColor(raw: string): Vec3 | null {\n  const s = raw.trim();\n  if (s.startsWith(\"#\")) {\n    const hex = s.slice(1);\n    if (hex.length === 3) {\n      const r = parseInt(hex[0] + hex[0], 16);\n      const g = parseInt(hex[1] + hex[1], 16);\n      const b = parseInt(hex[2] + hex[2], 16);\n      return Number.isNaN(r + g + b) ? null : [r, g, b];\n    }\n    if (hex.length >= 6) {\n      const r = parseInt(hex.slice(0, 2), 16);\n      const g = parseInt(hex.slice(2, 4), 16);\n      const b = parseInt(hex.slice(4, 6), 16);\n      return Number.isNaN(r + g + b) ? null : [r, g, b];\n    }\n    return null;\n  }\n  const m = s.match(/rgba?\\(\\s*([\\d.]+)[,\\s]+([\\d.]+)[,\\s]+([\\d.]+)/);\n  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;\n}\n\nfunction mix(a: Vec3, b: Vec3, t: number): Vec3 {\n  return [\n    Math.round(a[0] + (b[0] - a[0]) * t),\n    Math.round(a[1] + (b[1] - a[1]) * t),\n    Math.round(a[2] + (b[2] - a[2]) * t),\n  ];\n}\n\nfunction css(c: Vec3): string {\n  return `rgb(${c[0]},${c[1]},${c[2]})`;\n}\n\n// deterministic lattice hash → [0,1)\nfunction hash2(x: number, y: number, s: number): number {\n  let h = (x * 374761393 + y * 668265263 + s * 1442695041) | 0;\n  h = Math.imul(h ^ (h >>> 13), 1274126177);\n  return ((h ^ (h >>> 16)) >>> 0) / 4294967296;\n}\n\nfunction smooth(t: number): number {\n  return t * t * (3 - 2 * t);\n}\n\n// single-octave value noise on an integer lattice\nfunction vnoise(x: number, y: number, s: number): number {\n  const ix = Math.floor(x);\n  const iy = Math.floor(y);\n  const fx = x - ix;\n  const fy = y - iy;\n  const a = hash2(ix, iy, s);\n  const b = hash2(ix + 1, iy, s);\n  const c = hash2(ix, iy + 1, s);\n  const d = hash2(ix + 1, iy + 1, s);\n  const u = smooth(fx);\n  const v = smooth(fy);\n  return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;\n}\n\nfunction mulberry32(a: number) {\n  return () => {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nconst CELL = 3; // px — grain cell size\n// per-band [t1, t2] quantization thresholds over the 2-octave noise — tuned so\n// each stratum has a visibly different dark/mid/light balance\nconst THRESHOLDS: readonly [number, number][] = [\n  [0.46, 0.74], // dense dark topsoil\n  [0.3, 0.52], // light, porous\n  [0.5, 0.62], // mid-heavy\n  [0.36, 0.68], // balanced — the vein band\n  [0.42, 0.5], // light-flooded bedrock\n];\nconst EASE_RATE = 8; // depth += (target-depth)*(1-exp(-dt*8)) ≈ 120ms lag\nconst SETTLE_MS = 250; // forced-settle deadline after the last scroll event\nconst EPS = 0.1; // px — sleep epsilon on |depth - target|\nconst CHIP_LIFE = 400; // ms\nconst CHIP_BATCH = 12; // particles per emission\nconst CHIP_CAP = 120;\nconst EMIT_EVERY = 0.09; // s between 12-particle batches while cutting\nconst VEL_MIN = 2; // px/s — below this the bit is \"parked\", no spray\nconst MIN_TIP = 16; // scene px — resting sliver so the bit tip clears the top edge at rest\n\ntype Chip = { x: number; y: number; vx: number; vy: number; born: number };\n\nexport function CoreSampleScroll({\n  bands = DEFAULT_BANDS,\n  totalDepth = 128,\n  trackVh = 250,\n  className = \"\",\n}: {\n  /** strata content, top → bottom; band 4 (index 3) carries the accent vein */\n  bands?: CoreSampleBand[];\n  /** depth in meters the HUD reads at full scroll */\n  totalDepth?: number;\n  /** scroll-track height in vh; the sticky stage is always 100vh */\n  trackVh?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}) {\n  const uid = useId();\n  const rootRef = useRef<HTMLElement>(null);\n  const stageRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const labelLayerRef = useRef<HTMLDivElement>(null);\n  const depthElRef = useRef<HTMLDivElement>(null);\n  const hoverRef = useRef(-1); // hovered/focused band index, -1 = none\n  const wakeRef = useRef<() => void>(() => {});\n\n  const [active, setActive] = useState(0);\n  const [reduced, setReduced] = useState(false);\n\n  const fractions = useMemo(() => bandFractions(bands.length), [bands.length]);\n  const starts = useMemo(() => {\n    const s: number[] = [];\n    let acc = 0;\n    for (const f of fractions) {\n      s.push(acc);\n      acc += f;\n    }\n    return s;\n  }, [fractions]);\n\n  const veinIdx = Math.min(3, bands.length - 1);\n  // scene is taller than the stage so the 0.15x parallax has strata to reveal\n  const overscan = reduced ? 1 : 1 + (0.15 * Math.max(0, trackVh - 100)) / 100;\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const on = () => setReduced(mq.matches);\n    on();\n    mq.addEventListener(\"change\", on);\n    return () => mq.removeEventListener(\"change\", on);\n  }, []);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const stage = stageRef.current;\n    const canvas = canvasRef.current;\n    if (!stage || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    const atlas = document.createElement(\"canvas\");\n    const actx = atlas.getContext(\"2d\");\n    const grain = document.createElement(\"canvas\");\n    const gctx = grain.getContext(\"2d\");\n    if (!actx || !gctx) return;\n\n    const n = bands.length;\n    const rand = mulberry32(0xc02e);\n\n    // -- token inks: read at mount, re-derived live on theme flip ------------\n    let fg: Vec3 = [237, 237, 237];\n    let border: Vec3 = [46, 46, 46];\n    let muted: Vec3 = [143, 143, 143];\n    let accent: Vec3 = [0, 107, 255];\n    const derive = () => {\n      const cs = getComputedStyle(document.documentElement);\n      fg = parseColor(cs.getPropertyValue(\"--foreground\")) ?? fg;\n      border = parseColor(cs.getPropertyValue(\"--border\")) ?? border;\n      muted = parseColor(cs.getPropertyValue(\"--ns-muted\")) ?? muted;\n      accent = parseColor(cs.getPropertyValue(\"--ns-accent\")) ?? accent;\n    };\n    derive();\n\n    // -- sizing: explicit style.width/height — sticky inset does NOT size a\n    // replaced element; backing store is dpr-scaled separately ---------------\n    let w = 0;\n    let h = 0;\n    let dpr = 1;\n    let sized = false;\n    let sceneH = 0;\n\n    const bandAt = (f: number): number => {\n      for (let i = n - 1; i >= 0; i--) {\n        if (f >= (starts[i] ?? 0)) return i;\n      }\n      return 0;\n    };\n\n    // grain rendered ONCE per resize/theme into the band atlas, blitted per\n    // frame — never per-frame noise\n    const genAtlas = () => {\n      if (!sized) return;\n      atlas.width = Math.max(1, Math.round(w * dpr));\n      atlas.height = Math.max(1, Math.round(sceneH * dpr));\n      actx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n      const gw = Math.max(1, Math.ceil(w / CELL));\n      const gh = Math.max(1, Math.ceil(sceneH / CELL));\n      grain.width = gw;\n      grain.height = gh;\n      const img = gctx.createImageData(gw, gh);\n      const px = img.data;\n      // 3 gray levels interpolated between --border and --ns-muted\n      const levels: Vec3[] = [border, mix(border, muted, 0.5), muted];\n      for (let gy = 0; gy < gh; gy++) {\n        const f = ((gy + 0.5) * CELL) / sceneH;\n        const b = bandAt(Math.min(0.9999, f));\n        // per-band quantization thresholds — each band gets a distinct\n        // dark/mid/light density so strata read apart in monochrome\n        const th = THRESHOLDS[b % THRESHOLDS.length] ?? THRESHOLDS[0]!;\n        const t1 = th[0];\n        const t2 = th[1];\n        for (let gx = 0; gx < gw; gx++) {\n          // 2-octave value noise, 3px cells\n          const nn =\n            0.65 * vnoise(gx / 5, gy / 5, b * 31 + 7) +\n            0.35 * vnoise(gx / 2.2, gy / 2.2, b * 31 + 19);\n          const c = levels[nn < t1 ? 0 : nn < t2 ? 1 : 2] ?? border;\n          const o = (gy * gw + gx) * 4;\n          px[o] = c[0];\n          px[o + 1] = c[1];\n          px[o + 2] = c[2];\n          px[o + 3] = 255;\n        }\n      }\n      gctx.putImageData(img, 0, 0);\n      actx.imageSmoothingEnabled = false;\n      actx.drawImage(grain, 0, 0, w, sceneH);\n      actx.imageSmoothingEnabled = true;\n\n      // hairline band boundaries\n      actx.strokeStyle = css(fg);\n      actx.globalAlpha = 0.22;\n      actx.lineWidth = 1;\n      for (let i = 1; i < n; i++) {\n        const y = Math.round((starts[i] ?? 0) * sceneH) + 0.5;\n        actx.beginPath();\n        actx.moveTo(0, y);\n        actx.lineTo(w, y);\n        actx.stroke();\n      }\n      actx.globalAlpha = 1;\n\n      // exactly one accent vein: 1.5px --ns-accent polyline at a fixed depth\n      // inside band 4\n      const vy =\n        ((starts[veinIdx] ?? 0) + (fractions[veinIdx] ?? 0.2) * 0.55) * sceneH;\n      actx.strokeStyle = css(accent);\n      actx.lineWidth = 1.5;\n      actx.lineJoin = \"round\";\n      actx.beginPath();\n      for (let x = 0; x <= w; x += 8) {\n        const yy = vy + (vnoise(x / 42, 3.7, 991) - 0.5) * 12;\n        if (x === 0) actx.moveTo(x, yy);\n        else actx.lineTo(x, yy);\n      }\n      actx.stroke();\n    };\n\n    const resize = () => {\n      const rect = stage.getBoundingClientRect();\n      if (rect.width < 2 || rect.height < 2) {\n        sized = false; // zero-size stage guard — loop no-ops until real\n        return;\n      }\n      w = rect.width;\n      h = rect.height;\n      sceneH = h * overscan;\n      dpr = Math.min(2, window.devicePixelRatio || 1);\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n      canvas.width = Math.max(1, Math.round(w * dpr));\n      canvas.height = Math.max(1, Math.round(h * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      sized = true;\n      genAtlas();\n    };\n    resize();\n\n    // -- hot-path state: locals only ----------------------------------------\n    let raf = 0;\n    let last = 0;\n    let visible = true;\n    let tip = 0; // eased drill-tip depth, scene px\n    let lastScrollT = performance.now();\n    let emitAccum = 0;\n    let chips: Chip[] = []; // pruned array — never destination-in fades\n    let curBand = -1;\n\n    // one frame: explicit full clear of the dynamic layer, atlas blit under a\n    // camera transform, then drill + chip spray in scene coords\n    const draw = (now: number) => {\n      ctx.clearRect(0, 0, w, h);\n      const camProg = sceneH > 0 ? Math.min(1, Math.max(0, tip / sceneH)) : 0;\n      const camY = -camProg * (sceneH - h); // 0.15x parallax travel\n      const s = 1 + 0.06 * camProg; // scale 1.00 → 1.06\n      ctx.save();\n      ctx.translate(w / 2, h / 2);\n      ctx.scale(s, s);\n      ctx.translate(-w / 2, -h / 2);\n      ctx.translate(0, camY);\n\n      ctx.drawImage(atlas, 0, 0, w, sceneH);\n\n      // hover/focus affordance: 1px --foreground top-rule on the band\n      const hov = hoverRef.current;\n      if (hov >= 0 && hov < n) {\n        const y = Math.round((starts[hov] ?? 0) * sceneH) + 0.5;\n        ctx.strokeStyle = css(fg);\n        ctx.lineWidth = 1;\n        ctx.beginPath();\n        ctx.moveTo(0, y);\n        ctx.lineTo(w, y);\n        ctx.stroke();\n      }\n\n      // drill string + mono-weight chevron bit, center-left column. Rendered\n      // at max(tip, MIN_TIP) so the bit tip always clears the top edge at\n      // rest (mount has tip=0) — physics/HUD still track the real `tip`.\n      const dx = Math.round(w * 0.34);\n      const drawTip = Math.max(MIN_TIP, tip);\n      ctx.strokeStyle = css(fg);\n      ctx.lineCap = \"butt\";\n      ctx.lineJoin = \"miter\";\n      ctx.lineWidth = 2;\n      ctx.beginPath();\n      ctx.moveTo(dx, -h);\n      ctx.lineTo(dx, drawTip - 9);\n      ctx.stroke();\n      ctx.beginPath();\n      ctx.moveTo(dx - 7, drawTip - 10);\n      ctx.lineTo(dx, drawTip);\n      ctx.lineTo(dx + 7, drawTip - 10);\n      ctx.stroke();\n      // depth ticks along the drilled hole\n      ctx.globalAlpha = 0.3;\n      ctx.lineWidth = 1;\n      for (let y = 56; y < drawTip - 12; y += 56) {\n        ctx.beginPath();\n        ctx.moveTo(dx - 5, y + 0.5);\n        ctx.lineTo(dx + 5, y + 0.5);\n        ctx.stroke();\n      }\n      ctx.globalAlpha = 1;\n\n      // chip spray\n      ctx.fillStyle = css(muted);\n      for (let i = 0; i < chips.length; i++) {\n        const c = chips[i];\n        if (!c) continue;\n        ctx.globalAlpha = Math.max(0, 1 - (now - c.born) / CHIP_LIFE);\n        ctx.fillRect(c.x - 1, c.y - 1, 2, 2);\n      }\n      ctx.globalAlpha = 1;\n      ctx.restore();\n\n      // labels ride the same camera via an offset transform on their layer —\n      // never absolute canvas/page coords\n      const layer = labelLayerRef.current;\n      if (layer) layer.style.transform = `scale(${s}) translateY(${camY}px)`;\n    };\n\n    // -- reduced motion: static labeled cross-section at full depth ----------\n    if (reduced) {\n      const renderStatic = () => {\n        if (!sized) return;\n        tip = sceneH;\n        chips = [];\n        draw(performance.now());\n      };\n      renderStatic();\n      wakeRef.current = renderStatic; // label hover redraws the top-rule\n      const ro = new ResizeObserver(() => {\n        resize();\n        renderStatic();\n      });\n      ro.observe(stage);\n      const mo = new MutationObserver(() => {\n        derive();\n        genAtlas(); // regenerate the band atlas for the new theme\n        renderStatic();\n      });\n      mo.observe(document.documentElement, {\n        attributes: true,\n        attributeFilter: [\"class\"],\n      });\n      return () => {\n        ro.disconnect();\n        mo.disconnect();\n        wakeRef.current = () => {};\n      };\n    }\n\n    const spawnChips = (dx: number, now: number) => {\n      for (let i = 0; i < CHIP_BATCH && chips.length < CHIP_CAP; i++) {\n        chips.push({\n          x: dx + (rand() - 0.5) * 12,\n          y: tip - 2,\n          vx: (rand() - 0.5) * 170,\n          vy: -(40 + rand() * 110),\n          born: now,\n        });\n      }\n    };\n\n    const loop = (now: number) => {\n      raf = 0;\n      if (!visible || !sized || !root) {\n        last = 0;\n        return; // paused offscreen / zero-size — observers wake us\n      }\n      const dt = last === 0 ? 1 / 60 : Math.min(0.05, (now - last) / 1000);\n      last = now;\n\n      // scroll progress from track geometry\n      const rrect = root.getBoundingClientRect();\n      const range = Math.max(1, rrect.height - window.innerHeight);\n      const prog = Math.min(1, Math.max(0, -rrect.top / range));\n      const target = prog * sceneH;\n\n      const prev = tip;\n      if (now - lastScrollT > SETTLE_MS) {\n        tip = target; // forced-settle deadline — the HUD never ticks forever\n      } else {\n        tip += (target - tip) * (1 - Math.exp(-dt * EASE_RATE));\n      }\n      const vel = (tip - prev) / dt;\n\n      // chip spray while the bit is cutting\n      const dx = Math.round(w * 0.34);\n      if (Math.abs(vel) > VEL_MIN) {\n        emitAccum += dt;\n        while (emitAccum >= EMIT_EVERY) {\n          emitAccum -= EMIT_EVERY;\n          spawnChips(dx, now);\n        }\n      } else {\n        emitAccum = 0;\n      }\n      // integrate + prune in place\n      let keep = 0;\n      for (let i = 0; i < chips.length; i++) {\n        const c = chips[i];\n        if (!c || now - c.born > CHIP_LIFE) continue;\n        c.vy += 520 * dt;\n        c.x += c.vx * dt;\n        c.y += c.vy * dt;\n        chips[keep++] = c;\n      }\n      chips.length = keep;\n\n      // band crossings drive the panel crossfade (rare setState, off the\n      // per-frame path otherwise)\n      const f = sceneH > 0 ? Math.min(0.9999, Math.max(0, tip / sceneH)) : 0;\n      const b = bandAt(f);\n      if (b !== curBand) {\n        curBand = b;\n        setActive(b);\n      }\n\n      // HUD depth readout — direct DOM, ticks every frame while moving\n      const depthEl = depthElRef.current;\n      if (depthEl) {\n        const meters =\n          (sceneH > 0 ? Math.min(1, Math.max(0, tip / sceneH)) : 0) * totalDepth;\n        depthEl.textContent = `${meters.toFixed(2).padStart(6, \"0\")} m`;\n      }\n\n      draw(now);\n\n      // sleep: settled within epsilon AND the chip list is empty\n      if (Math.abs(tip - target) < EPS && chips.length === 0) {\n        last = 0;\n        return;\n      }\n      raf = requestAnimationFrame(loop);\n    };\n\n    const wake = () => {\n      if (!raf) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    wakeRef.current = wake;\n\n    const onScroll = () => {\n      lastScrollT = performance.now();\n      wake();\n    };\n    window.addEventListener(\"scroll\", onScroll, { passive: true });\n\n    const ro = new ResizeObserver(() => {\n      resize();\n      wake();\n    });\n    ro.observe(stage);\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible) wake();\n    });\n    io.observe(stage);\n    // live theme re-derive + atlas regeneration on documentElement class flip\n    const mo = new MutationObserver(() => {\n      derive();\n      genAtlas();\n      wake();\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n    const onVis = () => {\n      if (!document.hidden) wake();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    wake();\n\n    return () => {\n      cancelAnimationFrame(raf);\n      raf = 0;\n      window.removeEventListener(\"scroll\", onScroll);\n      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n      wakeRef.current = () => {};\n    };\n  }, [bands, fractions, starts, totalDepth, trackVh, overscan, veinIdx, reduced]);\n\n  const scrollToBand = (i: number) => {\n    if (reduced) {\n      document\n        .getElementById(`${uid}-panel-${i}`)\n        ?.scrollIntoView({ block: \"start\" });\n      return;\n    }\n    const root = rootRef.current;\n    if (!root) return;\n    const rect = root.getBoundingClientRect();\n    const top = window.scrollY + rect.top;\n    const range = Math.max(1, root.offsetHeight - window.innerHeight);\n    const f = (starts[i] ?? 0) + (fractions[i] ?? 0) * 0.5;\n    window.scrollTo({ top: top + f * range, behavior: \"smooth\" });\n  };\n\n  const labelButtons = (\n    <div\n      ref={labelLayerRef}\n      aria-hidden={false}\n      className=\"pointer-events-none absolute inset-0 z-10\"\n      style={{ transformOrigin: \"50% 50%\" }}\n    >\n      {bands.map((b, i) => (\n        <button\n          key={b.id}\n          type=\"button\"\n          onClick={() => scrollToBand(i)}\n          onMouseEnter={() => {\n            hoverRef.current = i;\n            wakeRef.current();\n          }}\n          onMouseLeave={() => {\n            hoverRef.current = -1;\n            wakeRef.current();\n          }}\n          onFocus={() => {\n            hoverRef.current = i;\n            wakeRef.current();\n          }}\n          onBlur={() => {\n            hoverRef.current = -1;\n            wakeRef.current();\n          }}\n          className=\"pointer-events-auto absolute left-4 -translate-y-1/2 rounded-sm bg-background/60 px-2 py-1 font-mono text-[10px] uppercase tracking-[0.2em] text-ns-muted transition-colors hover:text-foreground focus-visible:text-foreground focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n          style={{\n            top: `${((starts[i] ?? 0) + (fractions[i] ?? 0) / 2) * overscan * 100}%`,\n          }}\n        >\n          {String(i + 1).padStart(2, \"0\")} {b.label}\n        </button>\n      ))}\n    </div>\n  );\n\n  const panelCard = (b: CoreSampleBand, i: number, extra = \"\") => (\n    <article\n      key={b.id}\n      id={reduced ? `${uid}-panel-${i}` : undefined}\n      className={`rounded-md border border-border bg-surface p-6 ${extra}`}\n    >\n      <p className=\"font-mono text-[10px] uppercase tracking-[0.25em] text-ns-muted\">\n        {String(i + 1).padStart(2, \"0\")} / {b.label}\n      </p>\n      <h3 className=\"mt-3 text-xl font-semibold text-foreground\">{b.title}</h3>\n      <p className=\"mt-2 text-sm leading-relaxed text-ns-muted\">{b.body}</p>\n      <p className=\"mt-4 border-t border-border pt-3 font-mono text-xs text-ns-muted\">\n        {b.spec}\n      </p>\n    </article>\n  );\n\n  // -- reduced motion: no pin choreography — static cross-section + list ----\n  if (reduced) {\n    return (\n      <section className={`relative ${className}`}>\n        <div\n          ref={stageRef}\n          className=\"relative h-[80vh] min-h-[480px] overflow-hidden border-y border-border\"\n        >\n          <canvas\n            ref={canvasRef}\n            aria-hidden\n            className=\"pointer-events-none absolute left-0 top-0\"\n          />\n          {labelButtons}\n          <div className=\"pointer-events-none absolute right-6 top-6 z-10 rounded-sm bg-background/60 p-3 text-right font-mono\">\n            <p className=\"text-[10px] uppercase tracking-[0.25em] text-ns-muted\">\n              depth\n            </p>\n            <div className=\"mt-1 text-sm tabular-nums text-foreground\">\n              {`${totalDepth.toFixed(2).padStart(6, \"0\")} m`}\n            </div>\n            <p className=\"mt-1 text-[10px] uppercase tracking-[0.2em] text-ns-muted\">\n              full core · {bands.length} bands\n            </p>\n          </div>\n        </div>\n        <div className=\"mx-auto mt-8 grid max-w-2xl gap-4 px-6\">\n          {bands.map((b, i) => panelCard(b, i))}\n        </div>\n      </section>\n    );\n  }\n\n  return (\n    <section\n      ref={rootRef}\n      className={`relative ${className}`}\n      style={{ height: `${trackVh}vh` }}\n    >\n      <div\n        ref={stageRef}\n        className=\"sticky top-0 h-screen overflow-hidden border-y border-border\"\n      >\n        <canvas\n          ref={canvasRef}\n          aria-hidden\n          className=\"pointer-events-none absolute left-0 top-0\"\n        />\n        {labelButtons}\n\n        {/* HUD — fixed stage corner, mono, ticks every frame while moving */}\n        <div className=\"pointer-events-none absolute right-6 top-6 z-10 rounded-sm bg-background/60 p-3 text-right font-mono\">\n          <p className=\"text-[10px] uppercase tracking-[0.25em] text-ns-muted\">\n            depth\n          </p>\n          <div\n            ref={depthElRef}\n            className=\"mt-1 text-sm tabular-nums text-foreground\"\n          >\n            000.00 m\n          </div>\n          <p\n            className={`mt-1 text-[10px] uppercase tracking-[0.2em] transition-colors ${\n              active === veinIdx ? \"text-ns-accent\" : \"text-ns-muted\"\n            }`}\n          >\n            {bands[active]?.label ?? \"\"} · {active + 1}/{bands.length}\n          </p>\n        </div>\n\n        {/* content panels — right column, crossfade + 12px slide per band */}\n        <div className=\"absolute right-[4%] top-1/2 z-10 w-[45%] max-w-[560px] -translate-y-1/2\">\n          <div className=\"grid\">\n            {bands.map((b, i) => (\n              <div\n                key={b.id}\n                className={`col-start-1 row-start-1 transition-[opacity,transform] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] ${\n                  i === active\n                    ? \"translate-y-0 opacity-100\"\n                    : i < active\n                      ? \"pointer-events-none -translate-y-3 opacity-0\"\n                      : \"pointer-events-none translate-y-3 opacity-0\"\n                }`}\n                aria-hidden={i !== active}\n              >\n                {panelCard(b, i)}\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/scroll-story-strata.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": "loud",
    "tags": [
      "scroll",
      "story",
      "canvas",
      "hero",
      "section",
      "parallax",
      "noise"
    ],
    "instruction": "Build a pinned scroll story: a 250vh scroll track wrapping a position:sticky 100vh stage. One full-stage Canvas 2D layer draws a geological core cross-section: 5 strata bands (thickness 12-28% of stage height each), each filled with 2-octave value-noise grain (3px cells) quantized to 3 gray levels interpolated between --border and --ns-muted, with slightly different quantization thresholds per band so bands read distinct in monochrome; exactly one accent vein — a 1.5px --ns-accent polyline at a fixed depth inside band 4. Grain is rendered ONCE per resize/theme into an offscreen band atlas and blitted per frame — never per-frame noise. DRILL: center-left column with a mono-weight chevron bit and drill string stroked in --foreground plus faint depth ticks; depth target = scrollProgress * totalDepth and the rendered depth eases with depth += (target-depth)*(1-exp(-dt*8)) (~120ms lag), with a forced-settle deadline that snaps to target 250ms after the last scroll event so the HUD never ticks forever. While |depth velocity| > 2px/s the bit tip emits 12-particle chip-spray batches (life 400ms, gravity-integrated, pruned array with an explicit full clear per frame — no destination-in accumulation). CAMERA: the scene is overscanned ~1.22x and the canvas translates at 0.15x scroll delta plus a scale 1.00→1.06 across full progress; a DOM label layer rides the identical camera via an offset CSS transform (never absolute canvas/page coords). CONTENT: 5 DOM panels stacked in a right column (45% width, padded surface card rounded-md); when the eased depth crosses a band threshold the outgoing panel fades/slides -12px and the incoming fades/slides in from +12px over 350ms cubic-bezier(0.22,1,0.36,1). HUD: stage-corner font-mono depth readout in meters with 2 decimals updated via direct DOM every frame while moving; the band label gains --ns-accent only while inside the vein band. INTERACTION: band labels along the left edge are focusable buttons that scroll the track to that band; hovering/focusing a label draws a 1px --foreground top-rule on its band in the canvas. REDUCED MOTION: no pin choreography — a static labeled cross-section rendered at full depth, panels as a normal stacked list, HUD frozen. ENGINEERING: all strata/grain/vein/HUD inks derived from getComputedStyle tokens at mount with a MutationObserver on documentElement class re-deriving live and regenerating the band atlas on theme flip; canvas sized via explicit style.width/height with a dpr-scaled backing store (sticky inset does not size a replaced element); rAF sleeps when |depth - target| < 0.1px and the chip list is empty; IntersectionObserver pauses the loop offscreen; zero-size stage guard; scroll listener passive and removed with all observers/rAF on unmount; no React state on the hot path.",
    "rank": 4
  },
  "type": "registry:ui"
}