{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"dependencies": ["@react-three/fiber", "three"],
	"description": "400x400 Three.js particle field from SVG. lucide-react, react-icons, Heroicons, Tabler, Phosphor, and similar libs work out of the box. Cursor repel + spring home.",
	"files": [
		{
			"content": "\"use client\";\n\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport {\n  isValidElement,\n  type PointerEvent,\n  type ReactElement,\n  type RefObject,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { renderToStaticMarkup } from \"react-dom/server\";\nimport * as THREE from \"three\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SvgParticleProps {\n  className?: string;\n  svg: ReactElement | string;\n  width?: number;\n  height?: number;\n  color?: string;\n  particleGap?: number;\n  particleSize?: number;\n  maxParticles?: number;\n  repelRadius?: number;\n  repelStrength?: number;\n  returnStrength?: number;\n  damping?: number;\n}\n\ninterface MouseState {\n  active: boolean;\n  x: number;\n  y: number;\n}\n\ninterface ParticleFieldProps {\n  homePositions: Float32Array;\n  mouseRef: RefObject<MouseState>;\n  color: string;\n  particleSize: number;\n  repelRadius: number;\n  repelStrength: number;\n  returnStrength: number;\n  damping: number;\n}\n\nconst VERTEX_SHADER = `\nuniform float uPointSize;\nvoid main() {\n  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);\n  gl_PointSize = uPointSize;\n  gl_Position = projectionMatrix * mvPosition;\n}\n`;\n\nconst FRAGMENT_SHADER = `\nuniform vec3 uColor;\nvoid main() {\n  vec2 uv = gl_PointCoord - vec2(0.5);\n  float dist = length(uv);\n  float alpha = smoothstep(0.5, 0.0, dist);\n  alpha *= alpha;\n  gl_FragColor = vec4(uColor, alpha);\n}\n`;\n\nfunction resolveSvgToString(svg: ReactElement | string): string {\n  const raw = isValidElement(svg) ? renderToStaticMarkup(svg) : svg;\n  return raw.replace(/currentColor/g, \"white\");\n}\n\nfunction createSvgDataUrl(svg: string): string {\n  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n}\n\nasync function sampleSvgToParticles({\n  svg,\n  width,\n  height,\n  particleGap,\n  maxParticles,\n}: {\n  svg: string;\n  width: number;\n  height: number;\n  particleGap: number;\n  maxParticles: number;\n}): Promise<Float32Array> {\n  const image = await new Promise<HTMLImageElement>((resolve, reject) => {\n    const img = new Image();\n    img.onload = () => resolve(img);\n    img.onerror = () => reject(new Error(\"Failed to load SVG source\"));\n    img.src = createSvgDataUrl(svg);\n  });\n\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = width;\n  canvas.height = height;\n  const context = canvas.getContext(\"2d\");\n  if (!context) {\n    return new Float32Array();\n  }\n\n  context.clearRect(0, 0, width, height);\n\n  const maxDrawWidth = width * 0.78;\n  const maxDrawHeight = height * 0.78;\n  const imageRatio = image.width / image.height;\n  const targetRatio = maxDrawWidth / maxDrawHeight;\n\n  const drawWidth = imageRatio > targetRatio ? maxDrawWidth : maxDrawHeight * imageRatio;\n  const drawHeight = imageRatio > targetRatio ? maxDrawWidth / imageRatio : maxDrawHeight;\n\n  const drawX = (width - drawWidth) * 0.5;\n  const drawY = (height - drawHeight) * 0.5;\n  context.drawImage(image, drawX, drawY, drawWidth, drawHeight);\n\n  const pixels = context.getImageData(0, 0, width, height).data;\n  const points: number[] = [];\n\n  for (let y = 0; y < height; y += particleGap) {\n    for (let x = 0; x < width; x += particleGap) {\n      const alpha = pixels[(y * width + x) * 4 + 3] ?? 0;\n      if (alpha < 40) {\n        continue;\n      }\n      points.push(x - width * 0.5, height * 0.5 - y, 0);\n    }\n  }\n\n  if (points.length === 0) {\n    return new Float32Array();\n  }\n\n  const totalParticles = Math.floor(points.length / 3);\n  if (totalParticles <= maxParticles) {\n    return new Float32Array(points);\n  }\n\n  const step = Math.ceil(totalParticles / maxParticles);\n  const reduced: number[] = [];\n  for (let i = 0; i < totalParticles; i += step) {\n    const base = i * 3;\n    reduced.push(points[base] ?? 0, points[base + 1] ?? 0, points[base + 2] ?? 0);\n  }\n  return new Float32Array(reduced);\n}\n\nfunction ParticleField({\n  homePositions,\n  mouseRef,\n  color,\n  particleSize,\n  repelRadius,\n  repelStrength,\n  returnStrength,\n  damping,\n}: ParticleFieldProps) {\n  const pointsRef = useRef<THREE.Points<THREE.BufferGeometry, THREE.ShaderMaterial>>(null);\n\n  const particleState = useMemo(() => {\n    const homes = new Float32Array(homePositions);\n    const positions = new Float32Array(homePositions);\n    const velocities = new Float32Array(homePositions.length);\n    const geometry = new THREE.BufferGeometry();\n    geometry.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3));\n    return { geometry, homes, positions, velocities };\n  }, [homePositions]);\n\n  const material = useMemo(\n    () =>\n      new THREE.ShaderMaterial({\n        blending: THREE.AdditiveBlending,\n        depthWrite: false,\n        fragmentShader: FRAGMENT_SHADER,\n        transparent: true,\n        uniforms: {\n          uColor: { value: new THREE.Color(color) },\n          uPointSize: { value: particleSize },\n        },\n        vertexShader: VERTEX_SHADER,\n      }),\n    [color, particleSize],\n  );\n\n  useEffect(() => {\n    return () => {\n      particleState.geometry.dispose();\n      material.dispose();\n    };\n  }, [material, particleState.geometry]);\n\n  useFrame((_, delta) => {\n    const points = pointsRef.current;\n    if (!points) {\n      return;\n    }\n\n    const { active, x: mouseX, y: mouseY } = mouseRef.current;\n    const radiusSquared = repelRadius * repelRadius;\n    const speed = Math.min(delta * 60, 2.5);\n\n    for (let i = 0; i < particleState.positions.length; i += 3) {\n      const px = particleState.positions[i] ?? 0;\n      const py = particleState.positions[i + 1] ?? 0;\n      const hx = particleState.homes[i] ?? 0;\n      const hy = particleState.homes[i + 1] ?? 0;\n\n      let vx = particleState.velocities[i] ?? 0;\n      let vy = particleState.velocities[i + 1] ?? 0;\n\n      if (active) {\n        const dxMouse = px - mouseX;\n        const dyMouse = py - mouseY;\n        const distanceSquared = dxMouse * dxMouse + dyMouse * dyMouse;\n\n        if (distanceSquared < radiusSquared && distanceSquared > 0.0001) {\n          const distance = Math.sqrt(distanceSquared);\n          const influence = 1 - distance / repelRadius;\n          const force = influence * repelStrength * speed;\n          vx += (dxMouse / distance) * force;\n          vy += (dyMouse / distance) * force;\n        }\n      }\n\n      vx += (hx - px) * returnStrength * speed;\n      vy += (hy - py) * returnStrength * speed;\n\n      vx *= damping;\n      vy *= damping;\n\n      particleState.velocities[i] = vx;\n      particleState.velocities[i + 1] = vy;\n      particleState.positions[i] = px + vx;\n      particleState.positions[i + 1] = py + vy;\n    }\n\n    const positionAttribute = points.geometry.getAttribute(\"position\") as THREE.BufferAttribute;\n    positionAttribute.needsUpdate = true;\n  });\n\n  return <points ref={pointsRef} geometry={particleState.geometry} material={material} frustumCulled={false} />;\n}\n\nexport function SvgParticle({\n  className,\n  svg,\n  width = 400,\n  height = 400,\n  color = \"#d4c6ff\",\n  particleGap = 4,\n  particleSize = 3.4,\n  maxParticles = 4200,\n  repelRadius = 52,\n  repelStrength = 0.28,\n  returnStrength = 0.04,\n  damping = 0.9,\n}: SvgParticleProps) {\n  const mouseRef = useRef<MouseState>({ active: false, x: 0, y: 0 });\n  const [homePositions, setHomePositions] = useState<Float32Array>(() => new Float32Array());\n  const svgString = useMemo(() => resolveSvgToString(svg), [svg]);\n\n  useEffect(() => {\n    let isCancelled = false;\n\n    const buildParticles = async () => {\n      try {\n        const sampled = await sampleSvgToParticles({\n          height,\n          maxParticles: Math.max(200, Math.floor(maxParticles)),\n          particleGap: Math.max(2, Math.floor(particleGap)),\n          svg: svgString,\n          width,\n        });\n        if (!isCancelled) {\n          setHomePositions(sampled);\n        }\n      } catch {\n        if (!isCancelled) {\n          setHomePositions(new Float32Array());\n        }\n      }\n    };\n\n    buildParticles().catch(() => undefined);\n\n    return () => {\n      isCancelled = true;\n    };\n  }, [height, maxParticles, particleGap, svgString, width]);\n\n  const handlePointerMove = useCallback(\n    (event: PointerEvent<HTMLDivElement>) => {\n      const rect = event.currentTarget.getBoundingClientRect();\n      const localX = event.clientX - rect.left;\n      const localY = event.clientY - rect.top;\n      mouseRef.current.active = true;\n      mouseRef.current.x = localX - width * 0.5;\n      mouseRef.current.y = height * 0.5 - localY;\n    },\n    [height, width],\n  );\n\n  const handlePointerLeave = useCallback(() => {\n    mouseRef.current.active = false;\n  }, []);\n\n  return (\n    <div\n      className={cn(\"relative overflow-hidden rounded-2xl\", className)}\n      onPointerMove={handlePointerMove}\n      onPointerLeave={handlePointerLeave}\n      style={{ height, width }}\n    >\n      <Canvas\n        orthographic={true}\n        dpr={[1, 2]}\n        gl={{ alpha: true, antialias: true, powerPreference: \"high-performance\" }}\n        camera={{\n          bottom: -height * 0.5,\n          far: 1000,\n          left: -width * 0.5,\n          near: 0.1,\n          position: [0, 0, 100],\n          right: width * 0.5,\n          top: height * 0.5,\n          zoom: 1,\n        }}\n      >\n        {homePositions.length > 0 && (\n          <ParticleField\n            homePositions={homePositions}\n            mouseRef={mouseRef}\n            color={color}\n            particleSize={particleSize}\n            repelRadius={repelRadius}\n            repelStrength={repelStrength}\n            returnStrength={returnStrength}\n            damping={damping}\n          />\n        )}\n      </Canvas>\n    </div>\n  );\n}\n",
			"path": "registry/new-york/svg-particle/svg-particle.tsx",
			"type": "registry:component"
		}
	],
	"name": "svg-particle",
	"title": "SVG Particle",
	"type": "registry:component"
}
