{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"particle-field","type":"registry:block","title":"particle field","description":"\"hero atmosphere = 27\" — thousands of drifting points, additive glow, reacts to the cursor.","author":"Motion Menu","categories":["role-hero","webgl","3d","three","glsl"],"dependencies":["three"],"registryDependencies":["@motionmenu/motion-menu-runtime"],"meta":{"number":"27","slug":"particle-field","framework":"react","docs":"https://motion-menu-two.vercel.app/p/particle-field.md"},"files":[{"path":"motion-menu/particle-field.html","type":"registry:file","target":"motion-menu/particle-field.html","content":"<article class=\"card\">\n        <header><span class=\"num\">27</span><h3>Particle field</h3><div class=\"tags\"><i>Three.js</i><i>shader</i></div><a class=\"format-action\" data-format-link data-pattern-slug=\"particle-field\" href=\"/r/particle-field.json?framework=react\" title=\"Open the React registry output\">React ↗</a></header>\n        <div class=\"demo\"><canvas class=\"gl\" id=\"gl-particles\"></canvas></div>\n        <p class=\"order\">Say: <b>\"hero atmosphere = 27\"</b> — thousands of drifting points, additive glow, reacts to the cursor.</p>\n      </article>"},{"path":"motion-menu/particle-field.css","type":"registry:file","target":"motion-menu/particle-field.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\ncanvas.gl { width: 100%; height: 100%; display: block; position: absolute; inset: 0; }\n\n.ai-card .num { color: var(--acc2) !important; }\n\n.demo:has(.hascheck input:checked) .hasbtn { border-color: var(--acc); color: var(--acc); pointer-events: auto; opacity: 1; }"},{"path":"components/motion-menu/particle-field.tsx","type":"registry:component","content":"// @ts-nocheck — generated wrapper; inlined pattern JS is imperative DOM code, not authored TS.\n\"use client\";\n// particle field — 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\\\">27</span><h3>Particle field</h3><div class=\\\"tags\\\"><i>Three.js</i><i>shader</i></div><a class=\\\"format-action\\\" data-format-link data-pattern-slug=\\\"particle-field\\\" href=\\\"/r/particle-field.json?framework=react\\\" title=\\\"Open the React registry output\\\">React \\u2197</a></header>\\n        <div class=\\\"demo\\\"><canvas class=\\\"gl\\\" id=\\\"gl-particles\\\"></canvas></div>\\n        <p class=\\\"order\\\">Say: <b>\\\"hero atmosphere = 27\\\"</b> \\u2014 thousands of drifting points, additive glow, reacts to the cursor.</p>\\n      </article>\";\n\nexport function ParticleField() {\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        /* ---- 27 · particle field ---- */\n        webglDemo('gl-particles', ({ scene, camera }) => {\n          camera.position.z = 9;\n          const COUNT = 1400;\n          const positions = new Float32Array(COUNT * 3);\n          const sizes = new Float32Array(COUNT);\n          for (let i = 0; i < COUNT; i++) {\n            positions[i * 3] = (Math.random() - 0.5) * 16;\n            positions[i * 3 + 1] = (Math.random() - 0.5) * 10;\n            positions[i * 3 + 2] = (Math.random() - 0.5) * 10;\n            sizes[i] = 0.5 + Math.random() * 1.6;\n          }\n          const geo = new THREE.BufferGeometry();\n          geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n          geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n          const mat = new THREE.ShaderMaterial({\n            transparent: true, blending: THREE.AdditiveBlending, depthWrite: false,\n            uniforms: {\n              uTime: { value: 0 },\n              uColor: { value: new THREE.Color(0xb7ff2e) },\n              uPixelRatio: { value: Math.min(devicePixelRatio, 2) },\n            },\n            vertexShader: `\n              attribute float size;\n              uniform float uTime;\n              uniform float uPixelRatio;\n              varying float vAlpha;\n              void main() {\n                vec3 pos = position;\n                pos.y += sin(uTime * 0.6 + position.x * 0.5) * 0.35;\n                pos.x += cos(uTime * 0.4 + position.y * 0.4) * 0.25;\n                vec4 mv = modelViewMatrix * vec4(pos, 1.0);\n                gl_Position = projectionMatrix * mv;\n                gl_PointSize = size * uPixelRatio * (40.0 / -mv.z);\n                vAlpha = clamp(1.0 - (-mv.z / 22.0), 0.0, 1.0);\n              }`,\n            fragmentShader: `\n              precision mediump float;\n              uniform vec3 uColor;\n              varying float vAlpha;\n              void main() {\n                float d = length(gl_PointCoord - 0.5);\n                float circle = smoothstep(0.5, 0.0, d);\n                gl_FragColor = vec4(uColor, circle * vAlpha * 0.9);\n              }`,\n          });\n          const points = new THREE.Points(geo, mat);\n          scene.add(points);\n          return { update(t, delta) {\n            mat.uniforms.uTime.value = t;\n            points.rotation.y += (glMouse.x * 0.35 - points.rotation.y) * 0.04;\n            points.rotation.x += (-glMouse.y * 0.25 - points.rotation.x) * 0.04;\n          }};\n        });\n      } catch (err: any) {\n        console.warn(\"[motion-menu] particle-field 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 ParticleField;\n"}]}