{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dust-field",
  "title": "Dust Field",
  "description": "Turns any image or SVG into a reactive dust field that disperses on hover.",
  "dependencies": [
    "three"
  ],
  "devDependencies": [
    "@types/three"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/components/library/dust-field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as THREE from \"three\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype DustFieldProps = {\n  src: string\n  className?: string\n  width?: number\n  height?: number\n  pixelDensity?: number\n  radius?: number\n  radiusScale?: number\n  densityMode?: \"source\" | \"normalized\"\n  sampleSize?: number\n  scatter?: number\n  floatStrength?: number\n  pointSize?: number\n  densityMin?: number\n  optimized?: boolean\n  bleed?: number\n}\n\ntype ImageSample = {\n  width: number\n  height: number\n  positions: Float32Array\n  colors: Float32Array\n  sizes: Float32Array\n}\n\ntype ImageAnalysis = {\n  coverage: number\n  luminance: number\n}\n\ntype DustFieldSettings = {\n  pixelDensity: number\n  radius: number\n  scatter: number\n  floatStrength: number\n  pointSize: number\n  densityMin: number\n}\n\nconst DEFAULTS = {\n  pixelDensity: 1.5,\n  radius: 160,\n  scatter: 45,\n  floatStrength: 4,\n  pointSize: 3.6,\n  densityMin: 0.4,\n  optimized: false,\n  bleed: 0,\n  densityMode: \"normalized\",\n  sampleSize: 220,\n} as const\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(Math.max(value, min), max)\n}\n\nfunction resolveSource(src: string) {\n  const trimmed = src.trim()\n  if (trimmed.startsWith(\"<svg\")) {\n    const encoded = encodeURIComponent(trimmed)\n    return `data:image/svg+xml;charset=utf-8,${encoded}`\n  }\n  return src\n}\n\nasync function loadImage(src: string) {\n  const image = new Image()\n  image.crossOrigin = \"anonymous\"\n  image.src = resolveSource(src)\n  await image.decode()\n  return image\n}\n\nfunction analyzeImage(image: HTMLImageElement): ImageAnalysis {\n  const canvas = document.createElement(\"canvas\")\n  const sampleScale = Math.max(1, Math.round(Math.min(image.width, image.height) / 160))\n  canvas.width = Math.max(1, Math.round(image.width / sampleScale))\n  canvas.height = Math.max(1, Math.round(image.height / sampleScale))\n  const ctx = canvas.getContext(\"2d\")\n\n  if (!ctx) {\n    return { coverage: 1, luminance: 0.5 }\n  }\n\n  ctx.drawImage(image, 0, 0, canvas.width, canvas.height)\n  const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height)\n\n  let visible = 0\n  let total = 0\n  let luminance = 0\n\n  for (let i = 0; i < data.length; i += 4) {\n    const alpha = data[i + 3]\n    if (alpha > 12) {\n      visible += 1\n      const r = data[i] / 255\n      const g = data[i + 1] / 255\n      const b = data[i + 2] / 255\n      luminance += 0.2126 * r + 0.7152 * g + 0.0722 * b\n    }\n    total += 1\n  }\n\n  const coverage = total > 0 ? visible / total : 1\n  const avgLuminance = visible > 0 ? luminance / visible : 0.5\n  return { coverage, luminance: avgLuminance }\n}\n\nfunction getOptimizedSettings(image: HTMLImageElement, analysis: ImageAnalysis): DustFieldSettings {\n  const maxDim = Math.max(image.width, image.height)\n  const coverage = analysis.coverage\n  const sizeBias = clamp(2 + maxDim / 800, 2, 6)\n\n  const pixelDensity = clamp(sizeBias + (0.45 - Math.min(coverage, 0.45)) * 0.9, 2, 6)\n  const pointSize = clamp(1.4 - (coverage - 0.45) * 0.8, 0.9, 1.8)\n  const scatter = clamp(90 + (1 - coverage) * 120, 80, 180)\n  const floatStrength = clamp(1.2 + (1 - coverage) * 1.4, 0.6, 2.6)\n  const radius = clamp(maxDim * 0.75, 160, 260)\n  const densityMin = clamp(0.22 + (coverage - 0.5) * 0.1, 0.12, 0.35)\n\n  return {\n    pixelDensity,\n    radius,\n    scatter,\n    floatStrength,\n    pointSize,\n    densityMin,\n  }\n}\n\nfunction sampleImage(\n  image: HTMLImageElement,\n  pixelDensity: number,\n  densityMode: DustFieldProps[\"densityMode\"],\n  sampleSize: number\n): ImageSample {\n  const canvas = document.createElement(\"canvas\")\n  const supersampleScale = pixelDensity < 1 ? Math.min(1 / pixelDensity, 3) : 1\n  const maxDim = Math.max(image.width, image.height)\n  const normalizedScale = densityMode === \"normalized\" ? sampleSize / maxDim : 1\n  const baseWidth = Math.max(1, Math.round(image.width * normalizedScale))\n  const baseHeight = Math.max(1, Math.round(image.height * normalizedScale))\n  canvas.width = Math.round(baseWidth * supersampleScale)\n  canvas.height = Math.round(baseHeight * supersampleScale)\n  const ctx = canvas.getContext(\"2d\")\n\n  if (!ctx) {\n    return {\n      width: baseWidth,\n      height: baseHeight,\n      positions: new Float32Array(),\n      colors: new Float32Array(),\n      sizes: new Float32Array(),\n    }\n  }\n\n  ctx.drawImage(image, 0, 0, canvas.width, canvas.height)\n  const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height)\n\n  const positions: number[] = []\n  const colors: number[] = []\n  const sizes: number[] = []\n  const step = pixelDensity >= 1 ? Math.max(1, Math.round(pixelDensity)) : 1\n\n  for (let y = 0; y < canvas.height; y += step) {\n    for (let x = 0; x < canvas.width; x += step) {\n      const idx = (y * canvas.width + x) * 4\n      const alpha = data[idx + 3]\n      if (alpha < 12) continue\n      const r = data[idx] / 255\n      const g = data[idx + 1] / 255\n      const b = data[idx + 2] / 255\n\n      const baseX = x / supersampleScale\n      const baseY = y / supersampleScale\n\n      positions.push(baseX - baseWidth / 2)\n      positions.push(baseHeight / 2 - baseY)\n      positions.push(0)\n\n      colors.push(r, g, b)\n      sizes.push(0.7 + Math.random() * 1.1)\n    }\n  }\n\n  return {\n    width: baseWidth,\n    height: baseHeight,\n    positions: new Float32Array(positions),\n    colors: new Float32Array(colors),\n    sizes: new Float32Array(sizes),\n  }\n}\n\nexport function DustField({\n  src,\n  className,\n  width,\n  height,\n  pixelDensity = DEFAULTS.pixelDensity,\n  radius = DEFAULTS.radius,\n  radiusScale,\n  densityMode = DEFAULTS.densityMode,\n  sampleSize = DEFAULTS.sampleSize,\n  scatter = DEFAULTS.scatter,\n  floatStrength = DEFAULTS.floatStrength,\n  pointSize = DEFAULTS.pointSize,\n  densityMin = DEFAULTS.densityMin,\n  optimized = DEFAULTS.optimized,\n  bleed = DEFAULTS.bleed,\n}: DustFieldProps) {\n  const containerRef = React.useRef<HTMLDivElement | null>(null)\n  const canvasRef = React.useRef<HTMLCanvasElement | null>(null)\n  const rendererRef = React.useRef<THREE.WebGLRenderer | null>(null)\n  const sceneRef = React.useRef<THREE.Scene | null>(null)\n  const cameraRef = React.useRef<THREE.OrthographicCamera | null>(null)\n  const materialRef = React.useRef<THREE.ShaderMaterial | null>(null)\n  const pointsRef = React.useRef<THREE.Points | null>(null)\n  const sampleRef = React.useRef<ImageSample | null>(null)\n  const frameRef = React.useRef<number | null>(null)\n  const mouseRef = React.useRef(new THREE.Vector2(9999, 9999))\n  const mouseTargetRef = React.useRef(new THREE.Vector2(9999, 9999))\n  const mouseActiveRef = React.useRef(false)\n  const scaleRef = React.useRef(1)\n\n  React.useEffect(() => {\n    const container = containerRef.current\n    const canvas = canvasRef.current\n    if (!container || !canvas) return\n\n    const renderer = new THREE.WebGLRenderer({\n      canvas,\n      alpha: true,\n      antialias: true,\n    })\n    renderer.setClearColor(0x000000, 0)\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))\n    rendererRef.current = renderer\n\n    const scene = new THREE.Scene()\n    sceneRef.current = scene\n\n    const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, -1000, 1000)\n    camera.position.z = 10\n    cameraRef.current = camera\n\n    const handleResize = () => {\n      const rect = container.getBoundingClientRect()\n      const bleedValue = Math.max(0, bleed)\n      const renderWidth = rect.width + bleedValue * 2\n      const renderHeight = rect.height + bleedValue * 2\n      renderer.setSize(renderWidth, renderHeight, false)\n      camera.left = -renderWidth / 2\n      camera.right = renderWidth / 2\n      camera.top = renderHeight / 2\n      camera.bottom = -renderHeight / 2\n      camera.updateProjectionMatrix()\n\n      if (sampleRef.current) {\n        const { width: imgW, height: imgH } = sampleRef.current\n        const baseScale = Math.min(rect.width / imgW, rect.height / imgH)\n        const renderScale = Math.min(renderWidth / rect.width, renderHeight / rect.height)\n        const scale = baseScale * renderScale\n        scaleRef.current = Number.isFinite(scale) ? scale : 1\n        if (materialRef.current) {\n          materialRef.current.uniforms.uScale.value = scaleRef.current\n          if (radiusScale !== undefined) {\n            const scaledRadius = Math.max(1, Math.round(Math.min(rect.width, rect.height) * radiusScale))\n            materialRef.current.uniforms.uRadius.value = scaledRadius\n          }\n        }\n      }\n    }\n\n    const observer = new ResizeObserver(handleResize)\n    observer.observe(container)\n    handleResize()\n\n    const onPointerMove = (event: PointerEvent) => {\n      const rect = container.getBoundingClientRect()\n      mouseActiveRef.current = true\n      mouseTargetRef.current.x = event.clientX - rect.left - rect.width / 2\n      mouseTargetRef.current.y = rect.height / 2 - (event.clientY - rect.top)\n    }\n\n    const onPointerLeave = () => {\n      mouseActiveRef.current = false\n    }\n\n    window.addEventListener(\"pointermove\", onPointerMove)\n    window.addEventListener(\"pointerleave\", onPointerLeave)\n\n    const clock = new THREE.Clock()\n    const tick = () => {\n      frameRef.current = requestAnimationFrame(tick)\n      if (materialRef.current) {\n        materialRef.current.uniforms.uTime.value = clock.getElapsedTime()\n        const followSpeed = 0.12\n        const releaseFollowSpeed = 0.03\n        const releaseTargetSpeed = 0.01\n        const farPoint = new THREE.Vector2(9999, 9999)\n\n        if (!mouseActiveRef.current) {\n          mouseTargetRef.current.lerp(farPoint, releaseTargetSpeed)\n          mouseRef.current.lerp(mouseTargetRef.current, releaseFollowSpeed)\n        } else {\n          mouseRef.current.lerp(mouseTargetRef.current, followSpeed)\n        }\n        const mouseUniform = materialRef.current.uniforms.uMouse.value as THREE.Vector2\n        mouseUniform.copy(mouseRef.current)\n      }\n      renderer.render(scene, camera)\n    }\n\n    tick()\n\n    return () => {\n      if (frameRef.current) cancelAnimationFrame(frameRef.current)\n      observer.disconnect()\n      window.removeEventListener(\"pointermove\", onPointerMove)\n      window.removeEventListener(\"pointerleave\", onPointerLeave)\n      renderer.dispose()\n      materialRef.current?.dispose()\n      pointsRef.current?.geometry.dispose()\n      scene.clear()\n    }\n  }, [bleed, radiusScale])\n\n  React.useEffect(() => {\n    let isActive = true\n    const scene = sceneRef.current\n    if (!scene) return\n\n    const setup = async () => {\n      const image = await loadImage(src)\n      if (!isActive) return\n      const analysis = analyzeImage(image)\n        const resolved = optimized\n          ? getOptimizedSettings(image, analysis)\n          : {\n              pixelDensity,\n              radius,\n              scatter,\n              floatStrength,\n              pointSize,\n              densityMin,\n            }\n\n      const sample = sampleImage(image, resolved.pixelDensity, densityMode, sampleSize)\n      sampleRef.current = sample\n\n      const geometry = new THREE.BufferGeometry()\n      geometry.setAttribute(\"position\", new THREE.Float32BufferAttribute(sample.positions, 3))\n      geometry.setAttribute(\"initialPosition\", new THREE.Float32BufferAttribute(sample.positions, 3))\n      geometry.setAttribute(\"color\", new THREE.Float32BufferAttribute(sample.colors, 3))\n      geometry.setAttribute(\"size\", new THREE.Float32BufferAttribute(sample.sizes, 1))\n\n       const material = new THREE.ShaderMaterial({\n        transparent: true,\n        depthWrite: false,\n        vertexColors: true,\n        blending: THREE.NormalBlending,\n        uniforms: {\n          uTime: { value: 0 },\n          uMouse: { value: new THREE.Vector2(9999, 9999) },\n           uRadius: { value: resolved.radius },\n          uScatter: { value: resolved.scatter },\n          uFloat: { value: resolved.floatStrength },\n          uScale: { value: scaleRef.current },\n          uPointSize: { value: resolved.pointSize },\n          uBaseAlpha: { value: 1.0 },\n          uDensityMin: { value: resolved.densityMin },\n        },\n        vertexShader: `\n          uniform float uTime;\n          uniform vec2 uMouse;\n          uniform float uRadius;\n          uniform float uScatter;\n          uniform float uFloat;\n          uniform float uScale;\n          uniform float uPointSize;\n          uniform float uDensityMin;\n\n          attribute vec3 initialPosition;\n          attribute float size;\n          varying vec3 vColor;\n          varying float vInfluence;\n          varying float vSize;\n          varying float vAlive;\n\n          float hash(vec2 p) {\n            return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);\n          }\n\n          void main() {\n            vec3 base = initialPosition * uScale;\n            float dist = distance(base.xy, uMouse);\n            float falloff = smoothstep(uRadius, uRadius * 0.2, dist);\n            float influence = pow(falloff, 1.25);\n\n            vec2 flow = vec2(\n              sin(uTime * 0.55 + base.y * 0.035 + base.x * 0.01),\n              cos(uTime * 0.6 + base.x * 0.035 - base.y * 0.012)\n            );\n            vec2 swirl = vec2(\n              sin(uTime * 0.9 + base.x * 0.02),\n              cos(uTime * 0.8 + base.y * 0.02)\n            );\n            vec2 drift = normalize(flow + swirl) * uScatter;\n\n            float flutter = sin(uTime * 1.2 + base.x * 0.04 + base.y * 0.03);\n            vec3 floatOffset = vec3(\n              sin(uTime * 1.1 + base.y * 0.05),\n              cos(uTime * 1.3 + base.x * 0.05),\n              flutter\n            ) * uFloat;\n\n            vec3 displaced = base + vec3(drift * influence, 0.0) + floatOffset * (0.15 + 0.85 * influence);\n            displaced = mix(base, displaced, 0.85 + 0.15 * influence);\n\n            float density = mix(1.0, uDensityMin, influence);\n            float alive = step(hash(initialPosition.xy), density);\n\n            vColor = color;\n            vInfluence = influence;\n            vSize = size;\n            vAlive = alive;\n            gl_Position = projectionMatrix * modelViewMatrix * vec4(displaced, 1.0);\n            gl_PointSize = uPointSize * size * mix(0.95, 1.2, influence) * alive;\n          }\n        `,\n        fragmentShader: `\n          varying vec3 vColor;\n          varying float vInfluence;\n          varying float vSize;\n          varying float vAlive;\n          uniform float uBaseAlpha;\n          uniform float uBrightness;\n\n          void main() {\n            if (vAlive < 0.5) discard;\n            float dist = distance(gl_PointCoord, vec2(0.5));\n            float alpha = smoothstep(0.5, 0.0, dist);\n            float glow = mix(0.85, 1.2, vInfluence);\n            vec3 color = vColor * mix(0.95, 1.2, vInfluence);\n            float sizeAlpha = mix(0.7, 1.0, clamp(vSize, 0.0, 1.5));\n            gl_FragColor = vec4(color, alpha * glow * uBaseAlpha * sizeAlpha);\n          }\n        `,\n      })\n\n      const points = new THREE.Points(geometry, material)\n\n      if (pointsRef.current) {\n        scene.remove(pointsRef.current)\n        pointsRef.current.geometry.dispose()\n        materialRef.current?.dispose()\n      }\n\n      pointsRef.current = points\n      materialRef.current = material\n      scene.add(points)\n\n      const container = containerRef.current\n      if (container) {\n        const rect = container.getBoundingClientRect()\n        const bleedValue = Math.max(0, bleed)\n        const renderWidth = rect.width + bleedValue * 2\n        const renderHeight = rect.height + bleedValue * 2\n        const baseScale = Math.min(rect.width / sample.width, rect.height / sample.height)\n        const renderScale = Math.min(renderWidth / rect.width, renderHeight / rect.height)\n        const scale = baseScale * renderScale\n        scaleRef.current = Number.isFinite(scale) ? scale : 1\n        material.uniforms.uScale.value = scaleRef.current\n        if (radiusScale !== undefined) {\n          const scaledRadius = Math.max(1, Math.round(Math.min(rect.width, rect.height) * radiusScale))\n          material.uniforms.uRadius.value = scaledRadius\n        }\n      }\n    }\n\n    setup()\n\n    return () => {\n      isActive = false\n    }\n  }, [\n    src,\n    pixelDensity,\n    radius,\n    radiusScale,\n    densityMode,\n    sampleSize,\n    scatter,\n    floatStrength,\n    pointSize,\n    densityMin,\n    optimized,\n    bleed,\n  ])\n\n  React.useEffect(() => {\n    if (!materialRef.current) return\n    if (!optimized) {\n      if (radiusScale === undefined) {\n        materialRef.current.uniforms.uRadius.value = radius\n      }\n      materialRef.current.uniforms.uScatter.value = scatter\n      materialRef.current.uniforms.uFloat.value = floatStrength\n      materialRef.current.uniforms.uPointSize.value = pointSize\n      materialRef.current.uniforms.uBaseAlpha.value = 1.0\n      materialRef.current.uniforms.uDensityMin.value = densityMin\n    }\n  }, [\n    radius,\n    radiusScale,\n    scatter,\n    floatStrength,\n    pointSize,\n    densityMin,\n    optimized,\n  ])\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"relative h-full w-full overflow-visible\", className)}\n      style={{\n        width: width ? `${width}px` : undefined,\n        height: height ? `${height}px` : undefined,\n      }}\n    >\n      <canvas\n        ref={canvasRef}\n        className=\"absolute\"\n        style={{\n          inset: `${-Math.max(0, bleed)}px`,\n          width: `calc(100% + ${Math.max(0, bleed) * 2}px)`,\n          height: `calc(100% + ${Math.max(0, bleed) * 2}px)`,\n        }}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/library/dust-field.tsx"
    }
  ],
  "type": "registry:ui"
}