{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "marbled-paper",
  "title": "Marbled Paper",
  "description": "A living sheet of marbled paper in four traditional patterns: stone, combed, nonpareil and bouquet. Each pixel is traced through exact drop and comb maps, so edges stay sharp. It drifts slowly and you can drag through the ink like a stylus. Use it as a section or card background.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/marbled-paper/marbled-paper.tsx",
      "content": "\"use client\"\r\n\r\nimport * as React from \"react\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\n\r\nexport type MarbledPattern = \"stone\" | \"combed\" | \"nonpareil\" | \"bouquet\"\r\nexport type MarbledPalette = \"endpaper\" | \"nocturne\" | \"tide\" | \"ochre\"\r\n\r\n/** Pattern names in picker order. */\r\nexport const marbledPaperPatterns: readonly MarbledPattern[] = [\"stone\", \"combed\", \"nonpareil\", \"bouquet\"]\r\n\r\n/** Colour sets as hex. The first colour is the bath, the rest are dropped onto it as ink. */\r\nexport const marbledPaperPalettes: Record<MarbledPalette, string[]> = {\r\n  endpaper: [\"#f4f1ea\", \"#b8893a\", \"#387478\", \"#241d17\", \"#6e2a24\"],\r\n  nocturne: [\"#1c1713\", \"#b08d4a\", \"#387478\", \"#e8dfcc\", \"#6e2a24\"],\r\n  tide: [\"#eef0ea\", \"#387478\", \"#1d3a46\", \"#a9c4bf\", \"#b08d4a\"],\r\n  ochre: [\"#efe4cc\", \"#b8893a\", \"#7c4a23\", \"#241d17\", \"#d9b77a\"],\r\n}\r\n\r\ntype RGB = [number, number, number]\r\n\r\n/** Parses #rgb and #rrggbb into 0 to 1 channels. */\r\nfunction parseHex(input: string): RGB | null {\r\n  const hex = input.trim().replace(/^#/, \"\")\r\n  const full = hex.length === 3 ? hex.split(\"\").map((c) => c + c).join(\"\") : hex.slice(0, 6)\r\n  const n = Number.parseInt(full, 16)\r\n  if (full.length !== 6 || Number.isNaN(n)) return null\r\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255]\r\n}\r\n\r\n/** Seeded PRNG, so the server and the client lay down the same sheet. */\r\nfunction mulberry32(seed: number) {\r\n  let a = seed >>> 0\r\n  return () => {\r\n    a = (a + 0x6d2b79f5) | 0\r\n    let t = Math.imul(a ^ (a >>> 15), 1 | a)\r\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t\r\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296\r\n  }\r\n}\r\n\r\n/**\r\n * One step in making the sheet, after Jaffer and Lu's \"Mathematical Marbling\". Every step is an\r\n * area-preserving map with an exact inverse, so a pixel can be traced back through the steps to the ink\r\n * it came from.\r\n * - drop: ink spreads from a point and pushes everything already on the bath outwards.\r\n * - comb: a rake of parallel tines, `spacing` apart, drawn along (dx, dy). Ink moves by `shift` at a\r\n *   tine and half as far every `sharpness` units away from it.\r\n * - wave: a sine shear along (dx, dy). Wrapped around a comb it bends the tines into a serpentine.\r\n */\r\ntype Op =\r\n  | { kind: \"drop\"; x: number; y: number; r: number; color: number }\r\n  | { kind: \"comb\"; dx: number; dy: number; shift: number; sharpness: number; spacing: number; offset: number }\r\n  | { kind: \"wave\"; dx: number; dy: number; amplitude: number; frequency: number; phase: number }\r\n\r\n/** Sheet half-extents: the longer side of the element spans -1 to 1. */\r\nconst SHEET = 1\r\n\r\nfunction marble(pattern: MarbledPattern, seed: number, colorCount: number, scale: number, budget: number): Op[] {\r\n  const rand = mulberry32(seed * 7919 + 17)\r\n  const ops: Op[] = []\r\n  // Combing drags ink in from beyond the visible edge, so the bath has to be wider than the view.\r\n  const reach = pattern === \"stone\" ? 0.25 : 0.55\r\n  const extent = SHEET / scale + reach\r\n  const comb = pattern !== \"stone\"\r\n  const combSlots = comb ? 14 : 4\r\n\r\n  // Sprinkle one colour at a time over the whole bath, as a marbler does. Each round's drops push the\r\n  // earlier rounds aside into veins, and push each other into polygons, so the last colour forms the\r\n  // stones. Drops sit on a shuffled, jittered grid so the cover is even but the order is random.\r\n  const inks = Array.from({ length: colorCount - 1 }, (_, i) => i + 1)\r\n  for (let i = inks.length - 1; i > 0; i--) {\r\n    const j = Math.floor(rand() * (i + 1))\r\n    ;[inks[i], inks[j]] = [inks[j], inks[i]]\r\n  }\r\n  // The bath colour goes in early too, so pale veins run between the stones.\r\n  const rounds = [...inks]\r\n  rounds.splice(1 + Math.floor(rand() * (rounds.length - 1)), 0, 0)\r\n  // Early rounds end up as thin veins, so they need few drops; the last round makes the stones.\r\n  const drops = budget - combSlots\r\n  // Where each drop's centre has been pushed to by the drops after it, to keep new drops off old ones.\r\n  const live: { x: number; y: number; r: number; color: number }[] = []\r\n  rounds.forEach((color, round) => {\r\n    const last = round === rounds.length - 1\r\n    const share = last ? 0.55 : 0.45 / (rounds.length - 1)\r\n    const target = comb ? 0.2 : last ? 0.2 : 0.3\r\n    // Stones in the last round cost about 1.8 drops each with their liners.\r\n    const perCell = last && !comb ? 1.8 : 1\r\n    const cells = Math.max(3, Math.min(Math.ceil((2 * extent) / target), Math.floor(Math.sqrt((drops * share) / perCell))))\r\n    const cell = (2 * extent) / cells\r\n    const order = Array.from({ length: cells * cells }, (_, i) => i)\r\n    for (let i = order.length - 1; i > 0; i--) {\r\n      const j = Math.floor(rand() * (i + 1))\r\n      ;[order[i], order[j]] = [order[j], order[i]]\r\n    }\r\n    for (const index of order) {\r\n      // Each round spreads until its drops meet, as ink does on size, squeezing what came before into veins.\r\n      const r = cell * 0.5 * (0.75 + rand() * 0.5)\r\n      // The stones mix every ink, so neighbours differ and the sheet does not read as one colour.\r\n      const ink = last ? inks[Math.floor(rand() * inks.length)] : color\r\n      // Landing inside another colour rings the new drop with it, which is the look. Landing inside the\r\n      // same colour just swells that stone, so try a few spots in the cell and give up if all are taken.\r\n      let x = 0\r\n      let y = 0\r\n      let free = false\r\n      for (let attempt = 0; attempt < 6 && !free; attempt++) {\r\n        x = -extent + (index % cells + 0.1 + rand() * 0.8) * cell\r\n        y = -extent + (Math.floor(index / cells) + 0.1 + rand() * 0.8) * cell\r\n        free = live.every((d) => d.color !== ink || Math.hypot(d.x - x, d.y - y) > d.r * 1.05)\r\n      }\r\n      if (!free) continue\r\n      // Liners: a tiny drop of another ink under the stone is spread by it into a hairline ring, about\r\n      // r² / 2R wide. One or two of them give the banded outline of a battal stone.\r\n      let spread = r * r\r\n      if (last && !comb) {\r\n        const liners = rand() < 0.75 ? (rand() < 0.45 ? 2 : 1) : 0\r\n        let under = ink\r\n        for (let i = 0; i < liners; i++) {\r\n          under = (under + 1 + Math.floor(rand() * (colorCount - 1))) % colorCount\r\n          if (under === ink) under = (under + 1) % colorCount\r\n          const rl = r * (0.26 + rand() * 0.16)\r\n          ops.push({ kind: \"drop\", x, y, r: rl, color: under })\r\n          spread += rl * rl\r\n        }\r\n      }\r\n      for (const d of live) {\r\n        const dx = d.x - x\r\n        const dy = d.y - y\r\n        const k = Math.sqrt(1 + spread / Math.max(dx * dx + dy * dy, 1e-9))\r\n        d.x = x + dx * k\r\n        d.y = y + dy * k\r\n      }\r\n      live.push({ x, y, r: Math.sqrt(spread), color: ink })\r\n      ops.push({ kind: \"drop\", x, y, r, color: ink })\r\n    }\r\n  })\r\n\r\n  const tilt = (rand() - 0.5) * 0.12\r\n  const along = (angle: number) => ({ dx: Math.cos(angle + tilt), dy: Math.sin(angle + tilt) })\r\n  const offset = () => rand() * 2\r\n\r\n  if (comb) {\r\n    // Rake back and forth along x to pull the drops into long stripes.\r\n    const s1 = 0.13\r\n    const o1 = offset()\r\n    ops.push({ kind: \"comb\", ...along(0), shift: 0.42, sharpness: 0.035, spacing: s1, offset: o1 })\r\n    ops.push({ kind: \"comb\", ...along(0), shift: -0.42, sharpness: 0.035, spacing: s1, offset: o1 + s1 / 2 })\r\n    // Gel-git: a coarse rake out along y and back with the tines offset, folding the stripes into chevrons.\r\n    const s2 = 0.34\r\n    const o2 = offset()\r\n    const z2 = pattern === \"combed\" ? 0.2 : pattern === \"nonpareil\" ? 0.07 : 0.14\r\n    ops.push({ kind: \"comb\", ...along(Math.PI / 2), shift: z2, sharpness: 0.06, spacing: s2, offset: o2 })\r\n    ops.push({ kind: \"comb\", ...along(Math.PI / 2), shift: -z2, sharpness: 0.06, spacing: s2, offset: o2 + s2 / 2 })\r\n  }\r\n  if (pattern === \"nonpareil\") {\r\n    // One pass of a fine comb across the stripes pulls each into a row of small nested points.\r\n    ops.push({ kind: \"comb\", ...along(-Math.PI / 2), shift: 0.075, sharpness: 0.014, spacing: 0.045, offset: offset() })\r\n  }\r\n  if (pattern === \"bouquet\") {\r\n    // A fine comb drawn on a serpentine path: straighten the path, comb, bend it back.\r\n    const amplitude = 0.05\r\n    const frequency = 15\r\n    const phase = rand() * 6.283\r\n    ops.push({ kind: \"wave\", ...along(0), amplitude: -amplitude, frequency, phase })\r\n    ops.push({ kind: \"comb\", ...along(Math.PI / 2), shift: 0.11, sharpness: 0.016, spacing: 0.085, offset: offset() })\r\n    ops.push({ kind: \"wave\", ...along(0), amplitude, frequency, phase })\r\n  }\r\n  // A last slow shear so no stone is a perfect circle.\r\n  ops.push({ kind: \"wave\", ...along(rand() * 3.14), amplitude: 0.025, frequency: 2.4, phase: rand() * 6.283 })\r\n  return ops\r\n}\r\n\r\n/** Packs ops newest first, as the shader walks them backwards. Drops take one vec4, combs and waves two. */\r\nfunction packOps(ops: Op[], slots: number) {\r\n  const data = new Float32Array(slots * 4)\r\n  let k = 0\r\n  for (let i = ops.length - 1; i >= 0; i--) {\r\n    const op = ops[i]\r\n    const need = op.kind === \"drop\" ? 1 : 2\r\n    // Out of room: the oldest ops are the ones buried deepest under later ink, so they go first.\r\n    if (k + need > slots) break\r\n    const o = k * 4\r\n    if (op.kind === \"drop\") data.set([op.x, op.y, op.r, op.color], o)\r\n    else if (op.kind === \"comb\") data.set([op.dx, op.dy, op.shift, -1, op.sharpness, op.spacing, op.offset, 0], o)\r\n    else data.set([op.dx, op.dy, 0, -2, op.amplitude, op.frequency, op.phase, 0], o)\r\n    k += need\r\n  }\r\n  return { data, count: k }\r\n}\r\n\r\n/** Idle drift: two slow cross shears, each area preserving, like the size settling in the tray. */\r\nfunction drift(q: [number, number], amount: number, t: number) {\r\n  q[0] += amount * Math.sin(q[1] * 1.9 + t * 0.53)\r\n  q[1] += amount * Math.sin(q[0] * 1.6 - t * 0.41 + 1.7)\r\n}\r\n\r\n/** CPU twin of the fragment shader, for browsers without WebGL. Point sampled, so render it small. */\r\nfunction traceInk(ops: Op[], x: number, y: number): number {\r\n  let qx = x\r\n  let qy = y\r\n  for (let i = ops.length - 1; i >= 0; i--) {\r\n    const op = ops[i]\r\n    if (op.kind === \"drop\") {\r\n      const dx = qx - op.x\r\n      const dy = qy - op.y\r\n      const l = Math.hypot(dx, dy)\r\n      if (l < op.r) return op.color\r\n      const k = Math.sqrt(l * l - op.r * op.r) / l\r\n      qx = op.x + dx * k\r\n      qy = op.y + dy * k\r\n    } else if (op.kind === \"comb\") {\r\n      let d = qx * -op.dy + qy * op.dx - op.offset\r\n      d = Math.abs((((d + op.spacing / 2) % op.spacing) + op.spacing) % op.spacing - op.spacing / 2)\r\n      const e = op.shift * Math.pow(2, -d / op.sharpness)\r\n      qx -= op.dx * e\r\n      qy -= op.dy * e\r\n    } else {\r\n      const s = op.amplitude * Math.sin(op.frequency * (qx * -op.dy + qy * op.dx) + op.phase)\r\n      qx -= op.dx * s\r\n      qy -= op.dy * s\r\n    }\r\n  }\r\n  return 0\r\n}\r\n\r\nfunction paintFallback(canvas: HTMLCanvasElement, ops: Op[], colors: RGB[], width: number, height: number, scale: number) {\r\n  const ctx = canvas.getContext(\"2d\")\r\n  if (!ctx) return\r\n  // About 60k samples, whatever the size: enough to read as marbling, cheap enough to run once.\r\n  const k = Math.min(1, Math.sqrt(60000 / Math.max(1, width * height)))\r\n  const w = Math.max(1, Math.round(width * k))\r\n  const h = Math.max(1, Math.round(height * k))\r\n  canvas.width = w\r\n  canvas.height = h\r\n  const image = ctx.createImageData(w, h)\r\n  const long = Math.max(w, h)\r\n  const q: [number, number] = [0, 0]\r\n  for (let py = 0; py < h; py++) {\r\n    for (let px = 0; px < w; px++) {\r\n      q[0] = (((px + 0.5) * 2 - w) / long) * SHEET / scale\r\n      q[1] = ((h - (py + 0.5) * 2) / long) * SHEET / scale\r\n      drift(q, 0.02, 0)\r\n      const c = colors[traceInk(ops, q[0], q[1])] ?? colors[0]\r\n      const o = (py * w + px) * 4\r\n      image.data[o] = c[0] * 255\r\n      image.data[o + 1] = c[1] * 255\r\n      image.data[o + 2] = c[2] * 255\r\n      image.data[o + 3] = 255\r\n    }\r\n  }\r\n  ctx.putImageData(image, 0, 0)\r\n}\r\n\r\nconst VERTEX = `\r\nattribute vec2 aPos;\r\nvarying vec2 vUv;\r\nvoid main() {\r\n  vUv = aPos * 0.5 + 0.5;\r\n  gl_Position = vec4(aPos, 0.0, 1.0);\r\n}`\r\n\r\n/**\r\n * The comb field stores, per cell, how far back to look for the ink that sits there now. Two signed\r\n * 16-bit numbers packed into RGBA8, so it works on every WebGL1 device without float targets. Packed\r\n * values cannot be filtered by the GPU, hence the manual bilinear.\r\n */\r\nconst FIELD_LIB = `\r\nuniform sampler2D uField;\r\nuniform vec2 uTexel;\r\nfloat unpack(vec2 c) { return (floor(c.x * 255.0 + 0.5) * 256.0 + floor(c.y * 255.0 + 0.5) - 32768.0) / 8192.0; }\r\nvec2 fetch(vec2 uv) { vec4 t = texture2D(uField, uv); return vec2(unpack(t.rg), unpack(t.ba)); }\r\nvec2 field(vec2 uv) {\r\n  vec2 st = uv / uTexel - 0.5;\r\n  vec2 i = floor(st);\r\n  vec2 f = st - i;\r\n  vec2 b = (i + 0.5) * uTexel;\r\n  vec2 a = mix(fetch(b), fetch(b + vec2(uTexel.x, 0.0)), f.x);\r\n  vec2 c = mix(fetch(b + vec2(0.0, uTexel.y)), fetch(b + uTexel), f.x);\r\n  return mix(a, c, f.y);\r\n}`\r\n\r\nconst FIELD_FRAGMENT = `\r\nprecision highp float;\r\nvarying vec2 vUv;\r\nuniform vec2 uAspect;\r\nuniform vec2 uA;\r\nuniform vec2 uB;\r\nuniform float uWidth;\r\nuniform float uGain;\r\nuniform float uDecay;\r\n${FIELD_LIB}\r\nvec4 pack(vec2 v) {\r\n  vec2 n = clamp(floor(v * 8192.0 + 0.5) + 32768.0, 0.0, 65535.0);\r\n  vec2 hi = floor(n / 256.0);\r\n  vec2 lo = n - hi * 256.0;\r\n  return vec4(hi.x, lo.x, hi.y, lo.y) / 255.0;\r\n}\r\nvoid main() {\r\n  vec2 p = (vUv - 0.5) * 2.0 * uAspect;\r\n  vec2 v = uB - uA;\r\n  vec2 s = vec2(0.0);\r\n  float vv = dot(v, v);\r\n  if (uGain > 0.0 && vv > 0.0) {\r\n    float h = clamp(dot(p - uA, v) / vv, 0.0, 1.0);\r\n    float d = length(p - uA - v * h) / uWidth;\r\n    // Ink under the stylus travels with it; the wake falls off smoothly to either side.\r\n    s = v * uGain * exp2(-d * d);\r\n  }\r\n  // Semi-Lagrangian step: the ink here now is the ink that was one stylus step behind.\r\n  gl_FragColor = pack((field(vUv - s / (2.0 * uAspect)) - s) * uDecay);\r\n}`\r\n\r\nconst MARBLE_FRAGMENT = (slots: number) => `\r\nprecision highp float;\r\n#define SLOTS ${slots}\r\nvarying vec2 vUv;\r\nuniform vec4 uOps[SLOTS];\r\nuniform int uCount;\r\nuniform vec3 uColors[8];\r\nuniform vec2 uAspect;\r\nuniform float uPx;\r\nuniform float uScale;\r\nuniform float uTime;\r\nuniform float uDrift;\r\nuniform float uGrain;\r\nuniform float uFieldOn;\r\n${FIELD_LIB}\r\nvec3 ink(float k) {\r\n  vec3 c = uColors[0];\r\n  for (int j = 1; j < 8; j++) if (abs(float(j) - k) < 0.5) c = uColors[j];\r\n  return c;\r\n}\r\nfloat hash(vec2 p) {\r\n  p = fract(p * vec2(443.897, 441.423));\r\n  p += dot(p, p.yx + 19.19);\r\n  return fract((p.x + p.y) * p.x);\r\n}\r\nfloat noise(vec2 p) {\r\n  vec2 i = floor(p);\r\n  vec2 f = p - i;\r\n  f = f * f * (3.0 - 2.0 * f);\r\n  return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + 1.0), f.x), f.y);\r\n}\r\nvoid main() {\r\n  vec2 p = (vUv - 0.5) * 2.0 * uAspect;\r\n  float px = uPx / uScale;\r\n  mat2 J = mat2(px);\r\n  if (uFieldOn > 0.5) {\r\n    // A stroke stretches the sheet; carry that into the footprint so dragged threads stay smooth.\r\n    vec2 f0 = field(vUv);\r\n    vec2 e = uPx / (2.0 * uAspect);\r\n    vec2 fx = field(vUv + vec2(e.x, 0.0)) - f0;\r\n    vec2 fy = field(vUv + vec2(0.0, e.y)) - f0;\r\n    J = mat2(vec2(px, 0.0) + fx / uScale, vec2(0.0, px) + fy / uScale);\r\n    p += f0;\r\n  }\r\n  vec2 q = p / uScale;\r\n  q.x += uDrift * sin(q.y * 1.9 + uTime * 0.53);\r\n  q.y += uDrift * sin(q.x * 1.6 - uTime * 0.41 + 1.7);\r\n  vec2 q0 = q;\r\n\r\n  // Trace the pixel back through every step, newest first. The first drop it lands in is its colour.\r\n  // J carries the pixel's footprint through the steps (the Jacobian of the trace, in sheet units per\r\n  // pixel), so each drop edge is blended over exactly one pixel, front to back. Veins squeezed thinner\r\n  // than a pixel average out instead of shimmering, and everything else stays sharp.\r\n  vec3 col = vec3(0.0);\r\n  float T = 1.0;\r\n  bool skip = false;\r\n  for (int i = 0; i < SLOTS; i++) {\r\n    if (i >= uCount) break;\r\n    if (skip) { skip = false; continue; }\r\n    vec4 o = uOps[i];\r\n    if (o.w >= 0.0) {\r\n      vec2 d = q - o.xy;\r\n      float l = max(length(d), 1e-6);\r\n      vec2 n = d / l;\r\n      float r = o.z;\r\n      float we = max(length(n * J), px * 0.5);\r\n      float a = clamp((r - l) / we + 0.5, 0.0, 1.0);\r\n      if (a > 0.0) {\r\n        // Pigment gathers a little at the rim of a drop.\r\n        float rim = 1.0 - (0.03 + 0.12 * uGrain) * exp2(-max(r - l, 0.0) / (0.003 + px));\r\n        col += T * a * ink(o.w) * rim;\r\n        T *= 1.0 - a;\r\n        if (T < 0.004) break;\r\n      }\r\n      // Undo the drop: radially, l becomes sqrt(l^2 - r^2). Stretch along n, squeeze across it.\r\n      float le = max(l, r + 0.5 * we);\r\n      float g = max(sqrt(le * le - r * r) / le, 0.02);\r\n      q = o.xy + n * (g * le);\r\n      mat2 nn = mat2(n * n.x, n * n.y);\r\n      J = (g * (mat2(1.0) - nn) + nn / g) * J;\r\n    } else {\r\n      vec4 k = uOps[i + 1];\r\n      vec2 m = o.xy;\r\n      vec2 nrm = vec2(-m.y, m.x);\r\n      float dn = dot(q, nrm);\r\n      float slope;\r\n      if (o.w > -1.5) {\r\n        float t = mod(dn - k.z + 0.5 * k.y, k.y) - 0.5 * k.y;\r\n        float e = exp2(-abs(t) / k.x);\r\n        q -= m * (o.z * e);\r\n        slope = o.z * e * 0.6931 / k.x * sign(t);\r\n      } else {\r\n        float ph = k.y * dn + k.z;\r\n        q -= m * (k.x * sin(ph));\r\n        slope = -k.x * k.y * cos(ph);\r\n      }\r\n      J = (mat2(1.0) + slope * mat2(m * nrm.x, m * nrm.y)) * J;\r\n      skip = true;\r\n    }\r\n  }\r\n  col += T * uColors[0];\r\n\r\n  // Pigment mottle travels with the ink; paper tooth stays put on the sheet.\r\n  col *= 1.0 + uGrain * 0.06 * (noise(q0 * 40.0) - 0.5);\r\n  col += uGrain * 0.05 * (hash(floor(gl_FragCoord.xy)) - 0.5);\r\n  gl_FragColor = vec4(col, 1.0);\r\n}`\r\n\r\n/**\r\n * Contexts released on unmount, by canvas. React can remount onto the same canvas (StrictMode, fast\r\n * refresh), and that canvas hands back its lost context, so the remount has to restore it.\r\n */\r\nconst released = new WeakMap<HTMLCanvasElement, WEBGL_lose_context>()\r\n\r\nconst INTERACTIVE = \"a,button,input,select,textarea,label,summary,[role=button],[role=link],[contenteditable=true]\"\r\n\r\nexport interface MarbledPaperProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\r\n  /**\r\n   * \"stone\" is battal: drops pushed into each other. \"combed\" is gel-git: raked into stripes, then\r\n   * into chevrons. \"nonpareil\" adds a fine comb. \"bouquet\" draws the fine comb on a serpentine path.\r\n   * Default \"stone\".\r\n   */\r\n  pattern?: MarbledPattern\r\n  /** Hex colours, up to 8. The first is the bath, the rest are dropped onto it. Default the endpaper palette. */\r\n  colors?: string[]\r\n  /** Idle drift speed, 0 to 3. 0 holds the sheet still. Default 1. */\r\n  speed?: number\r\n  /** Drag through the ink to comb it like a stylus. Default true. */\r\n  interactive?: boolean\r\n  /** How much ink the stylus carries, 0 to 2. Faster strokes also pull a wider wake. Default 1. */\r\n  combStrength?: number\r\n  /** Seconds for a stroke to relax halfway back. 0 keeps strokes for good. Default 5. */\r\n  relax?: number\r\n  /** Size of the marbling, 0.5 to 2. Higher shows bigger stones. Default 1. */\r\n  scale?: number\r\n  /** Seed for the drop layout and comb positions. Change it to marble a new sheet. Default 1. */\r\n  seed?: number\r\n  /** Paper tooth, pigment mottle and rim darkening, 0 to 1. Default 0.5. */\r\n  paper?: number\r\n  /** Class names for the content wrapper around `children`. */\r\n  contentClassName?: string\r\n  /** Content laid over the paper. */\r\n  children?: React.ReactNode\r\n}\r\n\r\ntype Settings = {\r\n  speed: number\r\n  interactive: boolean\r\n  combStrength: number\r\n  relax: number\r\n  paper: number\r\n  scale: number\r\n  ops: Op[]\r\n  colors: RGB[]\r\n}\r\n\r\n/**\r\n * A living sheet of marbled paper. The pattern is traced per pixel through exact, area-preserving drop\r\n * and comb maps, so edges stay crisp at any size; dragging combs the ink with a stylus.\r\n */\r\nexport function MarbledPaper({\r\n  pattern = \"stone\",\r\n  colors = marbledPaperPalettes.endpaper,\r\n  speed = 1,\r\n  interactive = true,\r\n  combStrength = 1,\r\n  relax = 5,\r\n  scale = 1,\r\n  seed = 1,\r\n  paper = 0.5,\r\n  contentClassName,\r\n  className,\r\n  style,\r\n  children,\r\n  ...props\r\n}: MarbledPaperProps) {\r\n  const rootRef = React.useRef<HTMLDivElement>(null)\r\n  const canvasRef = React.useRef<HTMLCanvasElement>(null)\r\n  const fallbackRef = React.useRef<HTMLCanvasElement>(null)\r\n\r\n  // Keyed by value so an inline `colors={[...]}` does not redraw on every parent render.\r\n  const colorKey = colors.slice(0, 8).join(\",\")\r\n  const palette = React.useMemo(() => {\r\n    const parsed = colorKey.split(\",\").map((c) => parseHex(c) ?? ([0, 0, 0] as RGB))\r\n    return parsed.length >= 2 ? parsed : (marbledPaperPalettes.endpaper.map(parseHex) as RGB[])\r\n  }, [colorKey])\r\n  const size = Math.min(2, Math.max(0.5, scale))\r\n  const ops = React.useMemo(() => marble(pattern, seed, palette.length, size, 230), [pattern, seed, palette.length, size])\r\n\r\n  const settings = React.useRef<Settings>({\r\n    speed,\r\n    interactive,\r\n    combStrength,\r\n    relax,\r\n    paper,\r\n    scale: size,\r\n    ops,\r\n    colors: palette,\r\n  })\r\n  const wakeRef = React.useRef<(reset?: boolean, repaint?: boolean) => void>(() => {})\r\n\r\n  React.useEffect(() => {\r\n    const s = settings.current\r\n    // A new sheet starts with no strokes; a new palette keeps them.\r\n    const reset = s.ops !== ops || s.scale !== size\r\n    const repaint = reset || s.colors !== palette\r\n    Object.assign(s, { speed, interactive, combStrength, relax, paper, scale: size, ops, colors: palette })\r\n    wakeRef.current(reset, repaint)\r\n  }, [speed, interactive, combStrength, relax, paper, size, ops, palette])\r\n\r\n  React.useEffect(() => {\r\n    const root = rootRef.current\r\n    const canvas = canvasRef.current\r\n    const fallback = fallbackRef.current\r\n    if (!root || !canvas || !fallback) return\r\n    const cfg = settings.current\r\n\r\n    let gl: WebGLRenderingContext | null = null\r\n    let marbleProgram: WebGLProgram | null = null\r\n    let fieldProgram: WebGLProgram | null = null\r\n    let quad: WebGLBuffer | null = null\r\n    let textures: WebGLTexture[] = []\r\n    let buffers: WebGLFramebuffer[] = []\r\n    let current = 0\r\n    let slots = 0\r\n    let uploaded: Op[] | null = null\r\n    let lost = false\r\n\r\n    let width = 0\r\n    let height = 0\r\n    let fieldW = 1\r\n    let fieldH = 1\r\n    let aspect: [number, number] = [1, 1]\r\n    let fieldOn = false\r\n    let energy = 0\r\n    let time = 0\r\n    let raf = 0\r\n    let last = 0\r\n    let dirty = true\r\n    // Render scale, lowered step by step if frames keep running long on a slow GPU.\r\n    let quality = 1\r\n    let slow = 0\r\n    let pending = 0\r\n    const colorData = new Float32Array(24)\r\n    let colorsFor: RGB[] | null = null\r\n    let shown = false\r\n    let visible = true\r\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\r\n\r\n    // Pointer state lives here, never in React state.\r\n    let active: number | null = null\r\n    let rect: DOMRect | null = null\r\n    let prev: { x: number; y: number; t: number } | null = null\r\n    const strokes: { ax: number; ay: number; bx: number; by: number; dt: number }[] = []\r\n\r\n    const uniforms = new Map<string, WebGLUniformLocation | null>()\r\n    const loc = (program: WebGLProgram, name: string) => {\r\n      const key = (program === marbleProgram ? \"m:\" : \"f:\") + name\r\n      if (!uniforms.has(key)) uniforms.set(key, gl!.getUniformLocation(program, name))\r\n      return uniforms.get(key) ?? null\r\n    }\r\n\r\n    const compile = (g: WebGLRenderingContext, fragment: string) => {\r\n      const make = (type: number, source: string) => {\r\n        const shader = g.createShader(type)!\r\n        g.shaderSource(shader, source)\r\n        g.compileShader(shader)\r\n        return shader\r\n      }\r\n      const program = g.createProgram()!\r\n      const vs = make(g.VERTEX_SHADER, VERTEX)\r\n      const fs = make(g.FRAGMENT_SHADER, fragment)\r\n      g.attachShader(program, vs)\r\n      g.attachShader(program, fs)\r\n      g.bindAttribLocation(program, 0, \"aPos\")\r\n      g.linkProgram(program)\r\n      g.deleteShader(vs)\r\n      g.deleteShader(fs)\r\n      if (!g.getProgramParameter(program, g.LINK_STATUS)) {\r\n        g.deleteProgram(program)\r\n        return null\r\n      }\r\n      return program\r\n    }\r\n\r\n    const makeField = (g: WebGLRenderingContext, w: number, h: number) => {\r\n      const tex = g.createTexture()!\r\n      g.bindTexture(g.TEXTURE_2D, tex)\r\n      g.texParameteri(g.TEXTURE_2D, g.TEXTURE_MIN_FILTER, g.NEAREST)\r\n      g.texParameteri(g.TEXTURE_2D, g.TEXTURE_MAG_FILTER, g.NEAREST)\r\n      g.texParameteri(g.TEXTURE_2D, g.TEXTURE_WRAP_S, g.CLAMP_TO_EDGE)\r\n      g.texParameteri(g.TEXTURE_2D, g.TEXTURE_WRAP_T, g.CLAMP_TO_EDGE)\r\n      g.texImage2D(g.TEXTURE_2D, 0, g.RGBA, w, h, 0, g.RGBA, g.UNSIGNED_BYTE, null)\r\n      const fb = g.createFramebuffer()!\r\n      g.bindFramebuffer(g.FRAMEBUFFER, fb)\r\n      g.framebufferTexture2D(g.FRAMEBUFFER, g.COLOR_ATTACHMENT0, g.TEXTURE_2D, tex, 0)\r\n      return { tex, fb }\r\n    }\r\n\r\n    const clearField = () => {\r\n      if (!gl) return\r\n      for (const fb of buffers) {\r\n        gl.bindFramebuffer(gl.FRAMEBUFFER, fb)\r\n        gl.viewport(0, 0, fieldW, fieldH)\r\n        // Packed zero: 32768 is 128 high, 0 low, per axis.\r\n        gl.clearColor(128 / 255, 0, 128 / 255, 0)\r\n        gl.clear(gl.COLOR_BUFFER_BIT)\r\n      }\r\n      fieldOn = false\r\n      energy = 0\r\n    }\r\n\r\n    const init = () => {\r\n      const attrs: WebGLContextAttributes = {\r\n        alpha: false,\r\n        antialias: false,\r\n        depth: false,\r\n        stencil: false,\r\n        preserveDrawingBuffer: false,\r\n      }\r\n      const g = (canvas.getContext(\"webgl2\", attrs) ?? canvas.getContext(\"webgl\", attrs)) as WebGLRenderingContext | null\r\n      if (!g || g.isContextLost()) return false\r\n      const high = g.getShaderPrecisionFormat(g.FRAGMENT_SHADER, g.HIGH_FLOAT)\r\n      if (!high || high.precision < 16) return false\r\n      // Leave room for the other uniforms; very old GPUs only guarantee a few hundred vectors.\r\n      slots = Math.min(256, (g.getParameter(g.MAX_FRAGMENT_UNIFORM_VECTORS) as number) - 24)\r\n      if (slots < 48) return false\r\n      gl = g\r\n      marbleProgram = compile(g, MARBLE_FRAGMENT(slots))\r\n      fieldProgram = compile(g, FIELD_FRAGMENT)\r\n      if (!marbleProgram || !fieldProgram) return false\r\n      quad = g.createBuffer()\r\n      g.bindBuffer(g.ARRAY_BUFFER, quad)\r\n      g.bufferData(g.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), g.STATIC_DRAW)\r\n      g.enableVertexAttribArray(0)\r\n      g.vertexAttribPointer(0, 2, g.FLOAT, false, 0, 0)\r\n      uploaded = null\r\n      uniforms.clear()\r\n      textures = []\r\n      buffers = []\r\n      return true\r\n    }\r\n\r\n    const sizeField = () => {\r\n      if (!gl || !fieldProgram) return\r\n      // About one cell per 2 CSS px: the stylus wake is smooth, so it needs no more.\r\n      const k = Math.min(1 / 2, 512 / Math.max(width, height, 1))\r\n      const w = Math.max(2, Math.round(width * k))\r\n      const h = Math.max(2, Math.round(height * k))\r\n      if (w === fieldW && h === fieldH && textures.length) return\r\n      const old = textures.length ? { tex: textures[current], w: fieldW, h: fieldH } : null\r\n      const next = [makeField(gl, w, h), makeField(gl, w, h)]\r\n      const oldTextures = textures\r\n      const oldBuffers = buffers\r\n      textures = next.map((n) => n.tex)\r\n      buffers = next.map((n) => n.fb)\r\n      current = 0\r\n      fieldW = w\r\n      fieldH = h\r\n      if (old && fieldOn) {\r\n        // Carry existing strokes over to the new size instead of wiping them.\r\n        gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[0])\r\n        gl.viewport(0, 0, w, h)\r\n        gl.useProgram(fieldProgram)\r\n        gl.activeTexture(gl.TEXTURE0)\r\n        gl.bindTexture(gl.TEXTURE_2D, old.tex)\r\n        gl.uniform1i(loc(fieldProgram, \"uField\"), 0)\r\n        gl.uniform2f(loc(fieldProgram, \"uTexel\"), 1 / old.w, 1 / old.h)\r\n        gl.uniform2f(loc(fieldProgram, \"uAspect\"), aspect[0], aspect[1])\r\n        gl.uniform1f(loc(fieldProgram, \"uGain\"), 0)\r\n        gl.uniform1f(loc(fieldProgram, \"uDecay\"), 1)\r\n        gl.drawArrays(gl.TRIANGLES, 0, 3)\r\n      } else {\r\n        clearField()\r\n      }\r\n      oldTextures.forEach((t) => gl!.deleteTexture(t))\r\n      oldBuffers.forEach((b) => gl!.deleteFramebuffer(b))\r\n    }\r\n\r\n    const resize = () => {\r\n      const box = root.getBoundingClientRect()\r\n      width = Math.max(1, box.width)\r\n      height = Math.max(1, box.height)\r\n      const long = Math.max(width, height)\r\n      aspect = [width / long, height / long]\r\n      if (lost || !gl) {\r\n        if (!gl) paintFallback(fallback, cfg.ops, cfg.colors, width, height, cfg.scale)\r\n        return\r\n      }\r\n      // Cap the pixel count as well as the ratio: the trace costs per pixel, and big sections add up.\r\n      let dpr = Math.min(window.devicePixelRatio || 1, 2)\r\n      dpr = Math.min(dpr, Math.sqrt(2_000_000 / (width * height))) * quality\r\n      canvas.width = Math.max(1, Math.round(width * dpr))\r\n      canvas.height = Math.max(1, Math.round(height * dpr))\r\n      sizeField()\r\n      dirty = true\r\n      // Resizing clears the canvas, so repaint in the same task to avoid a blank frame.\r\n      render(0)\r\n    }\r\n\r\n    const fieldPass = (seg: { ax: number; ay: number; bx: number; by: number } | null, widthS: number, gain: number, decay: number) => {\r\n      const g = gl!\r\n      const program = fieldProgram!\r\n      const src = current\r\n      const dst = 1 - current\r\n      g.bindFramebuffer(g.FRAMEBUFFER, buffers[dst])\r\n      g.viewport(0, 0, fieldW, fieldH)\r\n      g.useProgram(program)\r\n      g.activeTexture(g.TEXTURE0)\r\n      g.bindTexture(g.TEXTURE_2D, textures[src])\r\n      g.uniform1i(loc(program, \"uField\"), 0)\r\n      g.uniform2f(loc(program, \"uTexel\"), 1 / fieldW, 1 / fieldH)\r\n      g.uniform2f(loc(program, \"uAspect\"), aspect[0], aspect[1])\r\n      g.uniform2f(loc(program, \"uA\"), seg?.ax ?? 0, seg?.ay ?? 0)\r\n      g.uniform2f(loc(program, \"uB\"), seg?.bx ?? 0, seg?.by ?? 0)\r\n      g.uniform1f(loc(program, \"uWidth\"), widthS)\r\n      g.uniform1f(loc(program, \"uGain\"), seg ? gain : 0)\r\n      g.uniform1f(loc(program, \"uDecay\"), decay)\r\n      g.drawArrays(g.TRIANGLES, 0, 3)\r\n      current = dst\r\n    }\r\n\r\n    const relaxing = () => fieldOn && cfg.relax > 0 && !reduced.matches\r\n    const drifting = () => cfg.speed > 0 && !reduced.matches\r\n    const animating = () => drifting() || strokes.length > 0 || relaxing()\r\n\r\n    const render = (dt: number) => {\r\n      const g = gl\r\n      if (!g || lost || !marbleProgram) return\r\n      if (drifting()) time += dt * Math.min(3, cfg.speed)\r\n      const pxS = 2 / Math.max(width, height)\r\n\r\n      // Comb strokes queued since the last frame, split so no step moves ink further than the wake is wide.\r\n      let decay = relaxing() ? Math.pow(0.5, dt / cfg.relax) : 1\r\n      if (strokes.length) {\r\n        let length = 0\r\n        let duration = 0\r\n        for (const s of strokes) {\r\n          length += Math.hypot(s.bx - s.ax, s.by - s.ay)\r\n          duration += s.dt\r\n        }\r\n        const speedPx = length / pxS / Math.max(duration / 1000, 1 / 120)\r\n        const fast = Math.min(1, Math.max(0, (speedPx - 150) / 1800))\r\n        const strength = Math.min(2, Math.max(0, cfg.combStrength))\r\n        // Ink right under the stylus keeps pace with it (gain near 1) and is drawn out into a long thread;\r\n        // fast strokes also pull a wider wake. Above 1 the ink would overtake the stylus and fold.\r\n        const widthS = 9 * pxS * (0.6 + 0.4 * strength) * (1 + 1.2 * fast)\r\n        const gain = Math.min(0.98, strength * (0.86 + 0.1 * fast))\r\n        const steps = Math.min(24, Math.max(strokes.length, Math.ceil(length / (widthS * 0.6))))\r\n        const per = length / steps\r\n        let passes = 0\r\n        for (const s of strokes) {\r\n          const len = Math.hypot(s.bx - s.ax, s.by - s.ay)\r\n          const n = Math.max(1, Math.round(len / Math.max(per, 1e-6)))\r\n          for (let i = 0; i < n && passes < 32; i++, passes++) {\r\n            const t0 = i / n\r\n            const t1 = (i + 1) / n\r\n            fieldPass(\r\n              {\r\n                ax: s.ax + (s.bx - s.ax) * t0,\r\n                ay: s.ay + (s.by - s.ay) * t0,\r\n                bx: s.ax + (s.bx - s.ax) * t1,\r\n                by: s.ay + (s.by - s.ay) * t1,\r\n              },\r\n              widthS,\r\n              gain,\r\n              decay\r\n            )\r\n            decay = 1\r\n          }\r\n        }\r\n        energy = Math.min(4, energy + length * gain)\r\n        strokes.length = 0\r\n        fieldOn = gain > 0 || fieldOn\r\n      } else if (decay < 1) {\r\n        fieldPass(null, 1, 0, decay)\r\n      }\r\n      energy *= decay\r\n      // Settled to well under a pixel: drop the field and skip its lookups.\r\n      if (fieldOn && cfg.relax > 0 && !reduced.matches && energy < pxS * 0.25) clearField()\r\n\r\n      if (uploaded !== cfg.ops) {\r\n        const packed = packOps(cfg.ops, slots)\r\n        g.useProgram(marbleProgram)\r\n        g.uniform4fv(loc(marbleProgram, \"uOps\"), packed.data)\r\n        g.uniform1i(loc(marbleProgram, \"uCount\"), packed.count)\r\n        uploaded = cfg.ops\r\n      }\r\n      const program = marbleProgram\r\n      g.bindFramebuffer(g.FRAMEBUFFER, null)\r\n      g.viewport(0, 0, canvas.width, canvas.height)\r\n      g.useProgram(program)\r\n      g.activeTexture(g.TEXTURE0)\r\n      g.bindTexture(g.TEXTURE_2D, textures[current] ?? null)\r\n      g.uniform1i(loc(program, \"uField\"), 0)\r\n      g.uniform2f(loc(program, \"uTexel\"), 1 / fieldW, 1 / fieldH)\r\n      if (colorsFor !== cfg.colors) {\r\n        colorData.fill(0)\r\n        cfg.colors.forEach((c, i) => colorData.set(c, i * 3))\r\n        colorsFor = cfg.colors\r\n      }\r\n      g.uniform3fv(loc(program, \"uColors\"), colorData)\r\n      g.uniform2f(loc(program, \"uAspect\"), aspect[0], aspect[1])\r\n      g.uniform1f(loc(program, \"uPx\"), (2 * aspect[0]) / canvas.width)\r\n      g.uniform1f(loc(program, \"uScale\"), cfg.scale)\r\n      g.uniform1f(loc(program, \"uTime\"), time)\r\n      g.uniform1f(loc(program, \"uDrift\"), 0.02)\r\n      g.uniform1f(loc(program, \"uGrain\"), Math.min(1, Math.max(0, cfg.paper)))\r\n      g.uniform1f(loc(program, \"uFieldOn\"), fieldOn ? 1 : 0)\r\n      g.drawArrays(g.TRIANGLES, 0, 3)\r\n      dirty = false\r\n\r\n      if (!shown) {\r\n        shown = true\r\n        canvas.style.opacity = \"1\"\r\n      }\r\n    }\r\n\r\n    const frame = (now: number) => {\r\n      raf = 0\r\n      const dt = Math.min(0.05, Math.max(0, (now - last) / 1000))\r\n      last = now\r\n      pending += dt\r\n      // Idle drift moves well under a pixel a frame, so 30fps is enough and halves the GPU cost.\r\n      // Strokes and relaxing follow the hand, so they get every frame.\r\n      const live = strokes.length > 0 || relaxing()\r\n      if (dirty || live || (drifting() && pending >= 1 / 30 - 0.004)) {\r\n        render(pending)\r\n        pending = 0\r\n      }\r\n      if (!animating()) return\r\n      // About a second of frames over 24ms: drop the resolution a notch, down to half.\r\n      slow = dt > 0.024 ? slow + 1 : Math.max(0, slow - 2)\r\n      if (slow > 50 && quality > 0.55) {\r\n        quality *= 0.85\r\n        slow = 0\r\n        resize()\r\n      }\r\n      schedule()\r\n    }\r\n    const schedule = () => {\r\n      if (raf || lost || !gl || !visible || document.visibilityState !== \"visible\") return\r\n      raf = requestAnimationFrame(frame)\r\n    }\r\n    const wake = (reset?: boolean, repaint?: boolean) => {\r\n      if (!gl) {\r\n        if (repaint) paintFallback(fallback, cfg.ops, cfg.colors, width, height, cfg.scale)\r\n        return\r\n      }\r\n      if (reset) clearField()\r\n      dirty = true\r\n      if (!raf) {\r\n        last = performance.now()\r\n        pending = 0\r\n      }\r\n      schedule()\r\n    }\r\n    wakeRef.current = wake\r\n\r\n    const toSheet = (e: PointerEvent) => {\r\n      const box = rect ?? root.getBoundingClientRect()\r\n      return {\r\n        x: (((e.clientX - box.left) / box.width) * 2 - 1) * aspect[0],\r\n        y: (1 - ((e.clientY - box.top) / box.height) * 2) * aspect[1],\r\n        t: e.timeStamp,\r\n      }\r\n    }\r\n    const onDown = (e: PointerEvent) => {\r\n      if (!cfg.interactive || !gl || active !== null) return\r\n      if (e.pointerType === \"mouse\" && e.button !== 0) return\r\n      if (e.target instanceof Element && e.target.closest(INTERACTIVE)) return\r\n      active = e.pointerId\r\n      rect = root.getBoundingClientRect()\r\n      prev = toSheet(e)\r\n      root.setPointerCapture?.(e.pointerId)\r\n      // Stops text selection under the stylus. Touch keeps its default so vertical scrolling still works.\r\n      if (e.pointerType === \"mouse\") e.preventDefault()\r\n    }\r\n    const onMove = (e: PointerEvent) => {\r\n      if (e.pointerId !== active || !prev) return\r\n      const events = e.getCoalescedEvents?.() ?? []\r\n      for (const ev of events.length ? events : [e]) {\r\n        const p = toSheet(ev)\r\n        if (p.x === prev.x && p.y === prev.y) continue\r\n        strokes.push({ ax: prev.x, ay: prev.y, bx: p.x, by: p.y, dt: Math.max(1, p.t - prev.t) })\r\n        prev = p\r\n      }\r\n      if (!raf) {\r\n        last = performance.now()\r\n        pending = 0\r\n      }\r\n      schedule()\r\n    }\r\n    const onUp = (e: PointerEvent) => {\r\n      if (e.pointerId !== active) return\r\n      active = null\r\n      prev = null\r\n      rect = null\r\n      if (root.hasPointerCapture?.(e.pointerId)) root.releasePointerCapture(e.pointerId)\r\n    }\r\n\r\n    const onLost = (e: Event) => {\r\n      e.preventDefault()\r\n      lost = true\r\n      if (raf) cancelAnimationFrame(raf)\r\n      raf = 0\r\n      canvas.style.visibility = \"hidden\"\r\n      paintFallback(fallback, cfg.ops, cfg.colors, width, height, cfg.scale)\r\n      fallback.style.display = \"block\"\r\n    }\r\n    const onRestored = () => {\r\n      lost = false\r\n      if (!init()) {\r\n        gl = null\r\n        return\r\n      }\r\n      canvas.style.visibility = \"\"\r\n      fallback.style.display = \"none\"\r\n      fieldW = fieldH = 0\r\n      resize()\r\n      schedule()\r\n    }\r\n\r\n    const io = new IntersectionObserver(([entry]) => {\r\n      visible = entry.isIntersecting\r\n      if (visible) wake()\r\n    })\r\n    const onVisibility = () => wake()\r\n    const onMotion = () => wake(true)\r\n    const ro = new ResizeObserver(resize)\r\n\r\n    const parked = released.get(canvas)\r\n    if (!init()) {\r\n      gl = null\r\n      fallback.style.display = \"block\"\r\n      if (parked && (canvas.getContext(\"webgl2\") ?? canvas.getContext(\"webgl\"))?.isContextLost()) {\r\n        // Show the CPU sheet until the restored event re-runs init.\r\n        lost = true\r\n        released.delete(canvas)\r\n        parked.restoreContext()\r\n      }\r\n    }\r\n    resize()\r\n    ro.observe(root)\r\n    io.observe(root)\r\n    document.addEventListener(\"visibilitychange\", onVisibility)\r\n    reduced.addEventListener(\"change\", onMotion)\r\n    canvas.addEventListener(\"webglcontextlost\", onLost)\r\n    canvas.addEventListener(\"webglcontextrestored\", onRestored)\r\n    root.addEventListener(\"pointerdown\", onDown)\r\n    root.addEventListener(\"pointermove\", onMove)\r\n    root.addEventListener(\"pointerup\", onUp)\r\n    root.addEventListener(\"pointercancel\", onUp)\r\n    root.addEventListener(\"lostpointercapture\", onUp)\r\n    schedule()\r\n\r\n    return () => {\r\n      if (raf) cancelAnimationFrame(raf)\r\n      wakeRef.current = () => {}\r\n      ro.disconnect()\r\n      io.disconnect()\r\n      document.removeEventListener(\"visibilitychange\", onVisibility)\r\n      reduced.removeEventListener(\"change\", onMotion)\r\n      canvas.removeEventListener(\"webglcontextlost\", onLost)\r\n      canvas.removeEventListener(\"webglcontextrestored\", onRestored)\r\n      root.removeEventListener(\"pointerdown\", onDown)\r\n      root.removeEventListener(\"pointermove\", onMove)\r\n      root.removeEventListener(\"pointerup\", onUp)\r\n      root.removeEventListener(\"pointercancel\", onUp)\r\n      root.removeEventListener(\"lostpointercapture\", onUp)\r\n      if (gl) {\r\n        textures.forEach((t) => gl!.deleteTexture(t))\r\n        buffers.forEach((b) => gl!.deleteFramebuffer(b))\r\n        gl.deleteBuffer(quad)\r\n        gl.deleteProgram(marbleProgram)\r\n        gl.deleteProgram(fieldProgram)\r\n        // Browsers cap live contexts; hand this one back now rather than at garbage collection.\r\n        const ext = gl.getExtension(\"WEBGL_lose_context\")\r\n        if (ext) {\r\n          ext.loseContext()\r\n          released.set(canvas, ext)\r\n        }\r\n      }\r\n    }\r\n  }, [])\r\n\r\n  return (\r\n    <div\r\n      ref={rootRef}\r\n      data-slot=\"marbled-paper\"\r\n      className={cn(\"relative isolate overflow-hidden\", className)}\r\n      style={{\r\n        backgroundColor: colors[0],\r\n        // Horizontal drags comb; vertical drags still scroll the page on touch.\r\n        touchAction: interactive ? \"pan-y pinch-zoom\" : undefined,\r\n        ...style,\r\n      }}\r\n      {...props}\r\n    >\r\n      <canvas\r\n        ref={fallbackRef}\r\n        aria-hidden\r\n        className=\"pointer-events-none absolute inset-0 hidden size-full\"\r\n      />\r\n      <canvas\r\n        ref={canvasRef}\r\n        aria-hidden\r\n        className=\"pointer-events-none absolute inset-0 block size-full opacity-0 transition-opacity duration-500 ease-[cubic-bezier(0.23,1,0.32,1)]\"\r\n      />\r\n      {children !== undefined && (\r\n        <div data-slot=\"marbled-paper-content\" className={cn(\"relative size-full\", contentClassName)}>\r\n          {children}\r\n        </div>\r\n      )}\r\n    </div>\r\n  )\r\n}\r\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "addedAt": "2026-10-27"
  },
  "type": "registry:component"
}