{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-scene",
  "type": "registry:component",
  "title": "Scroll Scene",
  "description": "A controlled SVG composition in three gestures. Supply scroll progress or a playhead; the scene follows the reader's pace and becomes a complete still under reduced motion.",
  "dependencies": [],
  "files": [
    {
      "path": "components/ScrollScene.tsx",
      "content": "/* Experience Design System influenced by Chromologium (https://chromologium.com/) */\n\"use client\";\nimport { useMemo } from \"react\";\nimport {\n  parseScrollScore,\n  scrollScoreSvg,\n  type ScrollScoreState,\n} from \"../lib/render/scrollScore\";\n\n/** Controlled scene: provide 0–100 progress from any native scroll container or range. */\nexport function ScrollScene({\n  state,\n  progress = 42,\n  still = false,\n}: {\n  state: ScrollScoreState;\n  progress?: number;\n  still?: boolean;\n}) {\n  const svg = useMemo(\n    () =>\n      scrollScoreSvg(\n        still ? { ...parseScrollScore(state), direction: \"gather\" } : state,\n        still ? 100 : progress,\n      ),\n    [state, progress, still],\n  );\n  return (\n    <div data-scroll-scene=\"\" style={{ width: \"100%\" }}>\n      <style>{`@media(prefers-reduced-motion:reduce){[data-scroll-scene] [data-score-piece]{transform:none!important}}`}</style>\n      <div dangerouslySetInnerHTML={{ __html: svg }} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/ScrollScene.tsx"
    },
    {
      "path": "lib/render/scrollScore.ts",
      "content": "/* Experience Design System influenced by Chromologium (https://chromologium.com/) */\n/** Authored choreography, not a physical simulation. All positions use an 800 × 640 stage. */\nexport interface ScrollScoreState {\n  direction: \"gather\" | \"release\";\n  easing: \"linear\" | \"smooth\";\n  travel: number;\n  stagger: number;\n  finish: number;\n}\nexport const SCROLL_SCORE_DEFAULT: ScrollScoreState = {\n  direction: \"gather\",\n  easing: \"smooth\",\n  travel: 70,\n  stagger: 12,\n  finish: 80,\n};\nexport function parseScrollScore(value: unknown): ScrollScoreState {\n  const v =\n    value && typeof value === \"object\"\n      ? (value as Record<string, unknown>)\n      : {};\n  const number = (key: string, min: number, max: number, fallback: number) =>\n    typeof v[key] === \"number\" && Number.isFinite(v[key])\n      ? Math.round(Math.max(min, Math.min(max, v[key] as number)))\n      : fallback;\n  return {\n    direction: v.direction === \"release\" ? \"release\" : \"gather\",\n    easing: v.easing === \"linear\" ? \"linear\" : \"smooth\",\n    travel: number(\"travel\", 0, 100, 70),\n    stagger: number(\"stagger\", 0, 25, 12),\n    finish: number(\"finish\", 55, 95, 80),\n  };\n}\nexport function scorePose(input: ScrollScoreState, progress: number) {\n  const state = parseScrollScore(input);\n  const p = Number.isFinite(progress)\n    ? Math.max(0, Math.min(100, progress))\n    : 0;\n  return [0, 1, 2].map((i) => {\n    const start = i * state.stagger;\n    const t = Math.max(0, Math.min(1, (p - start) / (state.finish - start)));\n    const eased = state.easing === \"smooth\" ? t * t * (3 - 2 * t) : t;\n    const distance =\n      (state.direction === \"gather\" ? 1 - eased : eased) * state.travel;\n    return {\n      x: (i - 1) * distance * 1.65,\n      y: (i === 1 ? -1.1 : 0.25) * distance,\n      rotation: (i - 1) * distance * 0.08,\n      complete: t,\n    };\n  });\n}\nexport const scoreTransform = (pose: ReturnType<typeof scorePose>[number]) =>\n  `translate(${pose.x.toFixed(3)} ${pose.y.toFixed(3)}) rotate(${pose.rotation.toFixed(3)} 400 315)`;\n\nexport function scrollScoreSvg(input: ScrollScoreState, progress = 42) {\n  const poses = scorePose(input, progress);\n  const accent = \"var(--ds-accent, #95682b)\";\n  const fg = \"var(--ds-fg-primary, #29251e)\";\n  const muted = \"var(--ds-fg-muted, #756c5e)\";\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 800 640\" role=\"img\" aria-label=\"Three architectural layers in an authored scroll sequence\" style=\"display:block;width:100%;height:auto;overflow:hidden\">\n  <rect width=\"800\" height=\"640\" fill=\"var(--ds-bg-primary, #f4efe4)\"/>\n  <g fill=\"none\" stroke=\"${fg}\" opacity=\".09\">${Array.from({ length: 15 }, (_, i) => `<path d=\"M${64 + i * 48} 80V536\"/>`).join(\"\")}${Array.from({ length: 10 }, (_, i) => `<path d=\"M64 ${80 + i * 48}H736\"/>`).join(\"\")}</g>\n  <g fill=\"${muted}\" font-family=\"monospace\" font-size=\"12\" letter-spacing=\"2\"><text x=\"32\" y=\"37\">CHOREOGRAPHY / 018</text><text x=\"768\" y=\"37\" text-anchor=\"end\">FORM · INTERVAL · ATTENTION</text></g>\n  <g data-score-piece=\"0\" transform=\"${scoreTransform(poses[0])}\">\n    <path d=\"M264 460V302a136 136 0 0 1 272 0v158h-53V302a83 83 0 0 0-166 0v158Z\" fill=\"${accent}\" opacity=\".82\"/>\n    <path d=\"M264 460V302a136 136 0 0 1 272 0v158\" fill=\"none\" stroke=\"${fg}\" stroke-width=\"1\"/>\n  </g>\n  <g data-score-piece=\"1\" transform=\"${scoreTransform(poses[1])}\">\n    <circle cx=\"400\" cy=\"302\" r=\"108\" fill=\"none\" stroke=\"${fg}\" stroke-width=\"1.2\"/>\n    <circle cx=\"400\" cy=\"302\" r=\"90\" fill=\"none\" stroke=\"${fg}\" opacity=\".4\"/>\n    <path d=\"M280 302H520M400 182V422\" stroke=\"${fg}\" stroke-width=\"1\"/>\n    <circle cx=\"400\" cy=\"302\" r=\"5\" fill=\"${fg}\"/>\n  </g>\n  <g data-score-piece=\"2\" transform=\"${scoreTransform(poses[2])}\">\n    <rect x=\"250\" y=\"420\" width=\"300\" height=\"52\" fill=\"${fg}\"/>\n    <g stroke=\"var(--ds-bg-primary, #f4efe4)\" opacity=\".45\">${Array.from({ length: 25 }, (_, i) => `<path d=\"M${258 + i * 12} 423v46\"/>`).join(\"\")}</g>\n    <path d=\"M250 482H550M250 477V487M550 477V487\" stroke=\"${fg}\"/>\n  </g>\n  <g fill=\"${fg}\" font-family=\"var(--chroma-heading-family, Georgia, serif)\"><text x=\"32\" y=\"577\" font-size=\"45\" font-style=\"italic\" textLength=\"720\" lengthAdjust=\"spacingAndGlyphs\">Give the eye somewhere to arrive.</text></g>\n  <path d=\"M32 599H768\" stroke=\"${fg}\" opacity=\".2\"/>\n  <g fill=\"${muted}\" font-family=\"monospace\" font-size=\"11\" letter-spacing=\"1.5\"><text x=\"32\" y=\"623\">I. THE ARCH</text><text x=\"307\" y=\"623\">II. THE MEASURE</text><text x=\"590\" y=\"623\">III. THE GROUND</text></g>\n  </svg>`;\n}\n\n/** Standalone, local-only HTML; sampled poses share the renderer's exact math. */\nexport function scrollScoreHtml(input: ScrollScoreState, dark = false) {\n  const state = parseScrollScore(input);\n  const frames = Array.from({ length: 101 }, (_, p) =>\n    scorePose(state, p).map(({ x, y, rotation }) =>\n      [x, y, rotation].map((n) => Number(n.toFixed(4))),\n    ),\n  );\n  const still = scrollScoreSvg({ ...state, direction: \"gather\" }, 100);\n  return `<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Scroll Score — Chromologium</title>\n<style>\n:root{color-scheme:${dark ? \"dark\" : \"light\"};--ds-bg-primary:${dark ? \"#20221f\" : \"#f4efe4\"};--ds-fg-primary:${dark ? \"#eee9dd\" : \"#29251e\"};--ds-fg-muted:${dark ? \"#b3b0a5\" : \"#756c5e\"};--ds-accent:${dark ? \"#d0b875\" : \"#95682b\"}}\n*{box-sizing:border-box}body{margin:0;background:var(--ds-bg-primary);color:var(--ds-fg-primary);font:18px/1.6 Georgia,serif}header,footer{max-width:850px;margin:auto;padding:50px 24px}h1{font-size:clamp(38px,7vw,80px);line-height:1.05;font-weight:400}a{color:inherit}.track{height:280vh;position:relative}.stage{position:sticky;top:0;height:100svh;display:grid;place-items:center}.stage>div{width:min(100%,900px);max-height:100%;aspect-ratio:800/640}.stage svg{max-height:100svh}output{position:fixed;bottom:12px;right:16px;background:var(--ds-bg-primary);padding:4px 12px;font:14px monospace}.motionHint{display:none}.enhanced .motionHint{display:inline}.enhanced .stillHint{display:none}.enhanced .still{display:none}.live{display:none}.enhanced .live{display:block}noscript{display:block;padding:1rem}body:not(.enhanced) .track{height:auto}body:not(.enhanced) .stage{height:auto;position:relative}.notes{display:grid;gap:24px;grid-template-columns:repeat(3,1fr)}@media(max-width:600px){.notes{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){.track{height:auto}.stage{height:auto;position:relative}.enhanced .live{display:none}.enhanced .still{display:block}.enhanced .motionHint{display:none}.enhanced .stillHint{display:inline}output{display:none}}\n</style><header><p>CHROMOLOGIUM / SCROLL SCORE</p><h1>Give the eye somewhere to arrive.</h1><p><span class=\"motionHint\">${state.direction === \"gather\" ? \"Scroll to move from the parts to the whole.\" : \"Scroll to open the structure and reveal its parts.\"} Your pace is the clock.</span><span class=\"stillHint\">The complete composition is shown as a still. Read its three parts below.</span></p></header><main><div class=\"track\"><div class=\"stage\"><div class=\"live\">${scrollScoreSvg(state, 0)}</div><div class=\"still\">${still}</div></div></div><noscript>The complete composition is shown above. The three parts remain readable without motion.</noscript><footer><div class=\"notes\"><p>I. The arch<br>A silhouette establishes the subject.</p><p>II. The measure<br>A fine line explains its proportions.</p><p>III. The ground<br>A base gives the composition a place to rest.</p></div><p>Authored geometric choreography; not a physical simulation. Finish at ${state.finish}% of the scroll interval, with ${state.stagger}% between starts.</p><a href=\"https://chromologium.com/lab/scroll-score\">Made at Chromologium’s Scroll Score</a></footer></main><output aria-label=\"Scroll progress\" aria-live=\"off\">0%</output><script type=\"application/json\" id=\"recipe\">${JSON.stringify(state)}</script><script>\nconst frames=${JSON.stringify(frames)};\nconst track=document.querySelector('.track'),pieces=document.querySelectorAll('.live [data-score-piece]'),readout=document.querySelector('output');\nconst reduced=matchMedia('(prefers-reduced-motion: reduce)');let queued=false;\nfunction render(){queued=false;if(reduced.matches)return;const span=track.offsetHeight-innerHeight;const p=Math.max(0,Math.min(100,span>0?-track.getBoundingClientRect().top/span*100:100));const a=Math.floor(p),b=Math.min(100,a+1),f=p-a;pieces.forEach((el,i)=>{const pose=frames[a][i].map((v,j)=>v+(frames[b][i][j]-v)*f);el.setAttribute('transform','translate('+pose[0]+' '+pose[1]+') rotate('+pose[2]+' 400 315)')});readout.textContent=Math.round(p)+'%'}\nfunction schedule(){if(!queued&&!reduced.matches){queued=true;requestAnimationFrame(render)}}\ndocument.body.classList.add('enhanced');addEventListener('scroll',schedule,{passive:true});addEventListener('resize',schedule);reduced.addEventListener('change',schedule);render();\n</script></html>`;\n}\n",
      "type": "registry:lib",
      "target": "lib/render/scrollScore.ts"
    }
  ],
  "docs": "Import ScrollScene and SCROLL_SCORE_DEFAULT. Pass state: ScrollScoreState and progress: number from 0 to 100. Optional still=true shows the complete composition. The component respects prefers-reduced-motion and inherits --ds-bg-primary, --ds-fg-primary, --ds-fg-muted, --ds-accent, and --chroma-heading-family, with standalone defaults. The consuming application owns scroll measurement. scrollScoreHtml(state,dark?) exports a standalone native-scroll example with a no-JavaScript and reduced-motion still. scorePose(state,progress) exposes three transforms for custom content. No loop, WebGL, provider or network required. Source: https://chromologium.com/lab/scroll-score.",
  "author": "The Chromologium",
  "homepage": "https://chromologium.com",
  "license": "MIT"
}
