{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"voronoi-shatter","type":"registry:block","title":"voronoi shatter","description":"\"transitions break with 159\" — imagery fractures into shards from your click point, then heals.","author":"Motion Menu","categories":["role-seam","signature","canvas"],"dependencies":[],"registryDependencies":["@motionmenu/motion-menu-runtime"],"meta":{"number":"159","slug":"voronoi-shatter","framework":"react","docs":"https://motion-menu-two.vercel.app/p/voronoi-shatter.md"},"files":[{"path":"motion-menu/voronoi-shatter.html","type":"registry:file","target":"motion-menu/voronoi-shatter.html","content":"<article class=\"card\">\n        <header><span class=\"num\">159</span><h3>Voronoi shatter</h3><div class=\"tags\"><i>canvas</i></div><a class=\"format-action\" data-format-link data-pattern-slug=\"voronoi-shatter\" href=\"/r/voronoi-shatter.json?framework=react\" title=\"Open the React registry output\">React ↗</a></header>\n        <div class=\"demo\"><canvas class=\"shatter-canvas\" id=\"shatter-canvas\"></canvas><p class=\"demo-hint\">click the image — it breaks where you strike</p></div>\n        <p class=\"order\">Say: <b>\"transitions break with 159\"</b> — imagery fractures into shards from your click point, then heals.</p>\n      </article>"},{"path":"motion-menu/voronoi-shatter.css","type":"registry:file","target":"motion-menu/voronoi-shatter.css","content":".card {\n    border: 1px solid var(--line); border-radius: 14px; background: var(--card);\n    overflow: hidden; display: flex; flex-direction: column;\n  }\n\n.card > header { display: flex; align-items: baseline; gap: .8rem; padding: 1.1rem 1.2rem .9rem; border-bottom: 1px solid var(--line); }\n\n.card .num { font-family: var(--mono); color: var(--acc); font-size: 1.05rem; }\n\n.card h3 { font-size: 1.02rem; font-weight: 600; letter-spacing: -0.01em; flex: 1; }\n\n.tags { display: flex; gap: .35rem; }\n\n.tags i {\n    font: 400 .62rem/1 var(--mono); font-style: normal; color: var(--dim);\n    border: 1px solid var(--line); border-radius: 99px; padding: .28em .6em; white-space: nowrap;\n  }\n\n.demo {\n    min-height: 250px; position: relative; display: flex; align-items: center; justify-content: center;\n    background: var(--bg2); overflow: hidden; flex: 1;\n  }\n\n.order { padding: .85rem 1.2rem; font-family: var(--mono); font-size: .72rem; color: var(--dim); border-top: 1px solid var(--line); }\n\n.order b { color: var(--txt); font-weight: 500; }\n\n.demo .btn.corner { position: absolute; top: 12px; right: 12px; padding: .55em 1em; font-size: .68rem; z-index: 5; }\n\n.demo-hint { position: absolute; bottom: 10px; left: 0; right: 0; text-align: center; font: 400 .65rem var(--mono); color: var(--dim); pointer-events: none; z-index: 4; }\n\n.ai-card .num { color: var(--acc2) !important; }\n\n/* ---------- Section Z · next wave vol. 2 ---------- */\n  .shatter-canvas, .phys-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }\n\n.demo:has(.hascheck input:checked) .hasbtn { border-color: var(--acc); color: var(--acc); pointer-events: auto; opacity: 1; }"},{"path":"components/motion-menu/voronoi-shatter.tsx","type":"registry:component","content":"// @ts-nocheck — generated wrapper; inlined pattern JS is imperative DOM code, not authored TS.\n\"use client\";\n// voronoi shatter — Motion Menu\n// Calls the Motion Menu shared runtime (webglDemo / canvas2dDemo / art helpers).\n// Install `motion-menu-runtime` and mount it once at the app root, or the effect\n// below throws ReferenceError on first paint.\nimport { useEffect, useRef } from \"react\";\n\nconst MARKUP = \"<article class=\\\"card\\\">\\n        <header><span class=\\\"num\\\">159</span><h3>Voronoi shatter</h3><div class=\\\"tags\\\"><i>canvas</i></div><a class=\\\"format-action\\\" data-format-link data-pattern-slug=\\\"voronoi-shatter\\\" href=\\\"/r/voronoi-shatter.json?framework=react\\\" title=\\\"Open the React registry output\\\">React \\u2197</a></header>\\n        <div class=\\\"demo\\\"><canvas class=\\\"shatter-canvas\\\" id=\\\"shatter-canvas\\\"></canvas><p class=\\\"demo-hint\\\">click the image \\u2014 it breaks where you strike</p></div>\\n        <p class=\\\"order\\\">Say: <b>\\\"transitions break with 159\\\"</b> \\u2014 imagery fractures into shards from your click point, then heals.</p>\\n      </article>\";\n\nexport function VoronoiShatter() {\n  const hostRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const host = hostRef.current;\n    if (!host) return;\n    const $ = (s: string) => host.querySelector(s) as HTMLElement | null;\n    let disposed = false;\n    const cleanup: Array<() => void> = [];\n\n    (async () => {\n      if (disposed) return;\n      try {\n        /* ---- 159 · voronoi shatter ---- */\n        canvas2dDemo('shatter-canvas', (d) => {\n          let art = null, artFor = '';\n          let shards = null, shatterT = 0;\n          d.canvas.addEventListener('pointerdown', (e) => {\n            if (shards || !d.w) return;\n            const r = d.canvas.getBoundingClientRect();\n            const ix = e.clientX - r.left, iy = e.clientY - r.top;\n            shards = [];\n            shatterT = 0;\n            const SPOKES = 9, RINGS = [0, 46, 120, Math.hypot(d.w, d.h)];\n            const angles = Array.from({ length: SPOKES }, (_, i) => (i / SPOKES) * Math.PI * 2 + Math.random() * 0.4);\n            for (let s = 0; s < SPOKES; s++) {\n              for (let ri = 0; ri < RINGS.length - 1; ri++) {\n                const a0 = angles[s], a1 = angles[(s + 1) % SPOKES] + (s === SPOKES - 1 ? Math.PI * 2 : 0);\n                const r0 = RINGS[ri] * (0.85 + Math.random() * 0.3), r1 = RINGS[ri + 1];\n                const poly = [\n                  [ix + Math.cos(a0) * r0, iy + Math.sin(a0) * r0], [ix + Math.cos(a1) * r0, iy + Math.sin(a1) * r0],\n                  [ix + Math.cos(a1) * r1, iy + Math.sin(a1) * r1], [ix + Math.cos(a0) * r1, iy + Math.sin(a0) * r1],\n                ];\n                const cx = poly.reduce((s2, p) => s2 + p[0], 0) / 4, cy = poly.reduce((s2, p) => s2 + p[1], 0) / 4;\n                const da = Math.atan2(cy - iy, cx - ix);\n                const dist = Math.hypot(cx - ix, cy - iy);\n                const speed = 340 / (1 + dist * 0.02);\n                shards.push({ poly, cx, cy, x: 0, y: 0, vx: Math.cos(da) * speed, vy: Math.sin(da) * speed - 60, rot: 0, vr: (Math.random() - 0.5) * 4 });\n              }\n            }\n          });\n          return (t, delta) => {\n            const key = d.w + 'x' + d.h;\n            if (key !== artFor && d.w) {\n              artFor = key;\n              art = makeArtCanvas(17);\n            }\n            if (!art) return;\n            const { ctx, w, h } = d;\n            ctx.clearRect(0, 0, w, h);\n            if (!shards) {\n              ctx.drawImage(art, 0, 0, w, h);\n              return;\n            }\n            shatterT += delta;\n            let alive = false;\n            shards.forEach((s) => {\n              s.vy += 900 * delta;\n              s.x += s.vx * delta; s.y += s.vy * delta; s.rot += s.vr * delta;\n              const alpha = Math.max(0, 1 - shatterT / 1.7);\n              if (alpha > 0) alive = true;\n              ctx.save();\n              ctx.globalAlpha = alpha;\n              ctx.translate(s.cx + s.x, s.cy + s.y);\n              ctx.rotate(s.rot);\n              ctx.translate(-s.cx, -s.cy);\n              ctx.beginPath();\n              s.poly.forEach(([px, py], i) => (i ? ctx.lineTo(px, py) : ctx.moveTo(px, py)));\n              ctx.closePath();\n              ctx.clip();\n              ctx.drawImage(art, 0, 0, w, h);\n              ctx.restore();\n            });\n            if (!alive) shards = null; // heal\n          };\n        });\n      } catch (err: any) {\n        console.warn(\"[motion-menu] voronoi-shatter failed to start:\", err);\n      }\n    })();\n\n    return () => {\n      disposed = true;\n      cleanup.forEach((fn) => { try { fn(); } catch (_) {} });\n    };\n  }, []);\n\n  return <div ref={hostRef} dangerouslySetInnerHTML={{ __html: MARKUP }} />;\n}\n\nexport default VoronoiShatter;\n"}]}