{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "particle-field",
  "type": "registry:component",
  "title": "ParticleField",
  "description": "6k GPU particles conjured into a glowing Fibonacci sphere with additive blending + bloom. Pass any palette to re-skin.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/postprocessing"
  ],
  "registryDependencies": [
    "https://prism.icglabs.co/r/canvas-perf.json"
  ],
  "tier": "free",
  "files": [
    {
      "path": "components/prism/ParticleField.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useRef, useState } from \"react\";\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport { EffectComposer, Bloom } from \"@react-three/postprocessing\";\nimport * as THREE from \"three\";\nimport { useDeviceTier } from \"@/lib/useDeviceTier\";\nimport { AutoDpr, PauseWhenOffscreen } from \"@/components/showcase/CanvasPerf\";\n\n/**\n * ParticleField — a GPU particle system that \"conjures\" 6k points from a\n * scattered cloud into a glowing Fibonacci sphere, then breathes and follows the\n * pointer. Custom GLSL + additive blending + bloom. Adapted from Sky's SkyScene\n * into a configurable primitive.\n *\n * Props let you re-skin it without touching the shader: pass any palette.\n */\nexport type ParticleFieldProps = {\n  count?: number;\n  /** 2–4 hex colors blended across the cloud. */\n  colors?: [string, string, ...string[]];\n  size?: number;\n  className?: string;\n};\n\nconst VERT = /* glsl */ `\nattribute vec3 aTarget;\nattribute vec3 aRand;\nattribute float aSeed;\nattribute vec3 aColor;\nuniform float uProgress;\nuniform float uTime;\nuniform float uSize;\nuniform float uPixelRatio;\nvarying vec3 vColor;\nvarying float vAlpha;\nvoid main() {\n  float pr = smoothstep(0.0, 1.0, uProgress);\n  vec3 pos = mix(aRand, aTarget, pr);\n  float d = (0.04 + 0.03 * aSeed) * pr;\n  pos.x += sin(uTime * 0.40 + aSeed * 6.2831) * d;\n  pos.y += cos(uTime * 0.35 + aSeed * 6.2831) * d;\n  pos.z += sin(uTime * 0.30 + aSeed * 3.1415) * d;\n  vec4 mv = modelViewMatrix * vec4(pos, 1.0);\n  gl_Position = projectionMatrix * mv;\n  gl_PointSize = uSize * uPixelRatio * (1.0 / -mv.z);\n  vColor = aColor;\n  vAlpha = mix(0.10, 0.95, pr);\n}\n`;\n\nconst FRAG = /* glsl */ `\nprecision mediump float;\nvarying vec3 vColor;\nvarying float vAlpha;\nvoid main() {\n  vec2 c = gl_PointCoord - 0.5;\n  float dd = dot(c, c);\n  if (dd > 0.25) discard;\n  float a = smoothstep(0.25, 0.0, dd) * vAlpha;\n  gl_FragColor = vec4(vColor, a);\n}\n`;\n\nfunction Conjure({\n  count,\n  colors,\n  size,\n  reduced,\n}: {\n  count: number;\n  colors: string[];\n  size: number;\n  reduced: boolean;\n}) {\n  const pts = useRef<THREE.Points>(null);\n  const mat = useRef<THREE.ShaderMaterial>(null);\n  const t0 = useRef(0);\n  const spin = useRef(0);\n\n  const geometry = useMemo(() => {\n    const position = new Float32Array(count * 3);\n    const aTarget = new Float32Array(count * 3);\n    const aRand = new Float32Array(count * 3);\n    const aSeed = new Float32Array(count);\n    const aColor = new Float32Array(count * 3);\n    const palette = colors.map((c) => new THREE.Color(c));\n    const R = 1.6;\n    const golden = Math.PI * (1 + Math.sqrt(5));\n    for (let i = 0; i < count; i++) {\n      const tt = i / count;\n      const phi = Math.acos(1 - 2 * tt);\n      const theta = golden * i;\n      const x = R * Math.sin(phi) * Math.cos(theta);\n      const y = R * Math.cos(phi);\n      const z = R * Math.sin(phi) * Math.sin(theta);\n      aTarget[i * 3] = x;\n      aTarget[i * 3 + 1] = y;\n      aTarget[i * 3 + 2] = z;\n      position[i * 3] = x;\n      position[i * 3 + 1] = y;\n      position[i * 3 + 2] = z;\n      const rr = 4 + Math.random() * 6;\n      const rp = Math.acos(2 * Math.random() - 1);\n      const rt = Math.random() * Math.PI * 2;\n      aRand[i * 3] = rr * Math.sin(rp) * Math.cos(rt);\n      aRand[i * 3 + 1] = rr * Math.cos(rp);\n      aRand[i * 3 + 2] = rr * Math.sin(rp) * Math.sin(rt);\n      aSeed[i] = Math.random();\n      // blend across the palette by vertical position, with an accent sprinkle\n      const f = (y / R + 1) / 2;\n      const seg = f * (palette.length - 1);\n      const lo = Math.floor(seg);\n      const hi = Math.min(lo + 1, palette.length - 1);\n      const col = palette[lo].clone().lerp(palette[hi], seg - lo);\n      if (palette.length > 2 && Math.random() < 0.14) {\n        col.lerp(palette[palette.length - 1], 0.6);\n      }\n      aColor[i * 3] = col.r;\n      aColor[i * 3 + 1] = col.g;\n      aColor[i * 3 + 2] = col.b;\n    }\n    const g = new THREE.BufferGeometry();\n    g.setAttribute(\"position\", new THREE.BufferAttribute(position, 3));\n    g.setAttribute(\"aTarget\", new THREE.BufferAttribute(aTarget, 3));\n    g.setAttribute(\"aRand\", new THREE.BufferAttribute(aRand, 3));\n    g.setAttribute(\"aSeed\", new THREE.BufferAttribute(aSeed, 1));\n    g.setAttribute(\"aColor\", new THREE.BufferAttribute(aColor, 3));\n    return g;\n  }, [count, colors]);\n\n  const uniforms = useMemo(\n    () => ({\n      uProgress: { value: 0 },\n      uTime: { value: 0 },\n      uSize: { value: size },\n      uPixelRatio: { value: 1 },\n    }),\n    [size]\n  );\n\n  useFrame((state, delta) => {\n    const m = mat.current;\n    if (m) {\n      if (t0.current === 0) t0.current = state.clock.elapsedTime;\n      const el = state.clock.elapsedTime - t0.current;\n      if (!reduced) m.uniforms.uTime.value = state.clock.elapsedTime;\n      m.uniforms.uProgress.value = reduced ? 1 : Math.min(1, el / 3.2);\n      m.uniforms.uPixelRatio.value = Math.min(state.gl.getPixelRatio(), 2);\n    }\n    const p = pts.current;\n    if (p && !reduced) {\n      spin.current += delta * 0.06;\n      p.rotation.y = spin.current + state.pointer.x * 0.35;\n      p.rotation.x = THREE.MathUtils.lerp(p.rotation.x, -state.pointer.y * 0.22, 0.06);\n    }\n  });\n\n  return (\n    <points ref={pts} geometry={geometry}>\n      <shaderMaterial\n        ref={mat}\n        uniforms={uniforms}\n        vertexShader={VERT}\n        fragmentShader={FRAG}\n        transparent\n        depthWrite={false}\n        blending={THREE.AdditiveBlending}\n      />\n    </points>\n  );\n}\n\nexport function ParticleField({\n  count = 6000,\n  colors = [\"#5b7cff\", \"#1fd4e6\", \"#8b5cff\", \"#ff3d81\"],\n  size = 18,\n  className,\n}: ParticleFieldProps) {\n  const reduced =\n    typeof window !== \"undefined\" &&\n    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  const q = useDeviceTier();\n  const [low, setLow] = useState(false);\n  const n = Math.max(800, Math.round(count * q.scale));\n  const bloomOn = q.bloom && !low;\n\n  return (\n    <div className={className} style={{ width: \"100%\", height: \"100%\" }}>\n      <Canvas\n        dpr={q.dpr}\n        gl={{ antialias: true, powerPreference: \"high-performance\" }}\n        camera={{ position: [0, 0, 5], fov: 50 }}\n      >\n        <Conjure count={n} colors={colors} size={size} reduced={reduced} />\n        <AutoDpr onLow={() => setLow(true)} />\n        <PauseWhenOffscreen />\n        {bloomOn && (\n          <EffectComposer>\n            <Bloom intensity={1.1} luminanceThreshold={0} luminanceSmoothing={0.25} mipmapBlur radius={0.65} />\n          </EffectComposer>\n        )}\n      </Canvas>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/prism/ParticleField.tsx"
    }
  ]
}