{
  "name": "particle-galaxy",
  "type": "registry:ui",
  "dependencies": [
    "three"
  ],
  "files": [
    {
      "path": "components/ui/particle-galaxy.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useEffect, useRef, useState } from \"react\"\nimport * as THREE from \"three\"\nimport { WebGLErrorBoundary, WebGLFallback } from \"./webgl-error-boundary\"\n\ninterface ParticleGalaxyProps {\n    className?: string\n    /** Number of particles in the galaxy (affects performance) */\n    particleCount?: number\n    /** Base size of particles */\n    particleSize?: number\n    /** Speed of automatic rotation */\n    rotationSpeed?: number\n    /** Number of spiral arms */\n    spiralArms?: number\n    /** Array of 3 color values for the galaxy */\n    colors?: [string, string, string]\n    /** Strength of mouse interaction (0-1) */\n    mouseInfluence?: number\n    /** Enable automatic rotation */\n    autoRotate?: boolean\n    /** Blend mode: 'additive' for dark backgrounds, 'normal' for light backgrounds */\n    blendMode?: \"additive\" | \"normal\"\n    /** How spread out the galaxy is (1-5) */\n    spread?: number\n    /** Particle density/opacity (0-1) */\n    density?: number\n    /** Glow intensity (0-100) */\n    glow?: number\n    /** Enable gentle camera movement */\n    cameraMovement?: boolean\n    /** Concentration of particles toward center (0-1) */\n    centerConcentration?: number\n    /** Enable particle pulsation animation */\n    pulsate?: boolean\n    /** Speed of particle pulsation (0.1-2) */\n    pulsateSpeed?: number\n    /** Enable mouse wheel zoom */\n    enableZoom?: boolean\n    /** Enable click and drag rotation */\n    enableDrag?: boolean\n    /** Enable touch gestures on mobile */\n    enableTouch?: boolean\n    /** Rotation damping factor (0-1) - higher = more responsive */\n    damping?: number\n    /** Min zoom distance */\n    minZoom?: number\n    /** Max zoom distance */\n    maxZoom?: number\n}\n\nexport function ParticleGalaxy({\n    className,\n    particleCount = 10000,\n    particleSize = 0.02,\n    rotationSpeed = 0.001,\n    spiralArms = 3,\n    colors: colorProp = [\"#4f46e5\", \"#8b5cf6\", \"#ec4899\"],\n    mouseInfluence = 0.5,\n    autoRotate = true,\n    blendMode = \"additive\",\n    spread = 2.5,\n    density = 0.7,\n    glow = 60,\n    cameraMovement = true,\n    centerConcentration = 0.5,\n    pulsate = true,\n    pulsateSpeed = 1,\n    enableZoom = true,\n    enableDrag = true,\n    enableTouch = true,\n    damping = 0.1,\n    minZoom = 1.5,\n    maxZoom = 10,\n}: ParticleGalaxyProps) {\n    const containerRef = useRef<HTMLDivElement>(null)\n    const canvasRef = useRef<HTMLCanvasElement>(null)\n    const rendererRef = useRef<THREE.WebGLRenderer | null>(null)\n    const sceneRef = useRef<THREE.Scene | null>(null)\n    const cameraRef = useRef<THREE.PerspectiveCamera | null>(null)\n    const particlesRef = useRef<THREE.Points | null>(null)\n    const mouseRef = useRef({ x: 0, y: 0 })\n    const frameRef = useRef<number | undefined>(undefined)\n    const [dimensions, setDimensions] = useState({ width: 0, height: 0 })\n    const [hasWebGLError, setHasWebGLError] = useState(false)\n\n    // 3D interaction state\n    const isDraggingRef = useRef(false)\n    const previousMouseRef = useRef({ x: 0, y: 0 })\n    const targetRotationRef = useRef({ x: 0, y: 0 })\n    const currentRotationRef = useRef({ x: 0, y: 0 })\n    const currentTiltRef = useRef({ x: 0, y: 0 })\n    const targetZoomRef = useRef(3)\n    const currentZoomRef = useRef(3)\n\n    useEffect(() => {\n        if (!containerRef.current || !canvasRef.current) return\n\n        const container = containerRef.current\n\n        // Get initial dimensions\n        const updateDimensions = () => {\n            const rect = container.getBoundingClientRect()\n            setDimensions({ width: rect.width, height: rect.height })\n        }\n\n        updateDimensions()\n\n        // Setup ResizeObserver\n        const resizeObserver = new ResizeObserver(updateDimensions)\n        resizeObserver.observe(container)\n\n        return () => {\n            resizeObserver.disconnect()\n        }\n    }, [])\n\n    useEffect(() => {\n        if (hasWebGLError) return\n        if (!canvasRef.current || !containerRef.current || dimensions.width === 0 || dimensions.height === 0)\n            return\n\n        try {\n            const canvas = canvasRef.current\n            const container = containerRef.current\n\n            // Scene\n            const scene = new THREE.Scene()\n            sceneRef.current = scene\n\n            // Camera\n            const camera = new THREE.PerspectiveCamera(\n                75,\n                dimensions.width / dimensions.height,\n                0.1,\n                1000\n            )\n            camera.position.z = 3\n            cameraRef.current = camera\n\n            // Renderer with transparent background\n            const renderer = new THREE.WebGLRenderer({\n                canvas,\n                alpha: true, // Transparent background\n                antialias: true,\n            })\n            renderer.setSize(dimensions.width, dimensions.height)\n            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))\n            rendererRef.current = renderer\n\n            // Create galaxy particles\n            const geometry = new THREE.BufferGeometry()\n            const positions = new Float32Array(particleCount * 3)\n            const colors = new Float32Array(particleCount * 3)\n            const scales = new Float32Array(particleCount)\n\n            // Convert hex colors to RGB\n            const colorPalette = [\n                new THREE.Color(colorProp[0]),\n                new THREE.Color(colorProp[1]),\n                new THREE.Color(colorProp[2]),\n            ]\n\n            for (let i = 0; i < particleCount; i++) {\n            const i3 = i * 3\n\n            // Spiral galaxy distribution with customizable spread\n            const radius = Math.pow(Math.random(), centerConcentration) * spread\n            const spinAngle = radius * spiralArms\n            const branchAngle =\n                ((i % spiralArms) / spiralArms) * Math.PI * 2 + spinAngle\n\n            // Randomness for organic feel\n            const randomnessStrength = 0.3 * (spread / 2.5)\n            const randomX =\n                Math.pow(Math.random(), 3) *\n                (Math.random() < 0.5 ? 1 : -1) *\n                randomnessStrength\n            const randomY =\n                Math.pow(Math.random(), 3) *\n                (Math.random() < 0.5 ? 1 : -1) *\n                randomnessStrength\n            const randomZ =\n                Math.pow(Math.random(), 3) *\n                (Math.random() < 0.5 ? 1 : -1) *\n                randomnessStrength\n\n            positions[i3] = Math.cos(branchAngle) * radius + randomX\n            positions[i3 + 1] = randomY\n            positions[i3 + 2] = Math.sin(branchAngle) * radius + randomZ\n\n            // Colors - mix based on distance from center\n            const mixedColor = colorPalette[i % 3]!.clone()\n            const centerDistance = radius / spread\n\n            // For normal blend mode (light backgrounds), darken colors\n            if (blendMode === \"normal\") {\n                mixedColor.lerp(new THREE.Color(\"#000000\"), centerDistance * 0.5)\n            } else {\n                // For additive blend mode (dark backgrounds), brighten toward center\n                mixedColor.lerp(new THREE.Color(\"#ffffff\"), 1 - centerDistance)\n            }\n\n            colors[i3] = mixedColor.r\n            colors[i3 + 1] = mixedColor.g\n            colors[i3 + 2] = mixedColor.b\n\n            // Variable scale for depth\n            scales[i] = Math.random()\n        }\n\n            geometry.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3))\n            geometry.setAttribute(\"color\", new THREE.BufferAttribute(colors, 3))\n            geometry.setAttribute(\"scale\", new THREE.BufferAttribute(scales, 1))\n\n            // Custom shader material for better particle rendering\n            const material = new THREE.ShaderMaterial({\n            uniforms: {\n                uTime: { value: 0 },\n                uSize: { value: particleSize * 100 },\n                uGlow: { value: glow / 100 },\n                uDensity: { value: density },\n                uPulsate: { value: pulsate ? 1.0 : 0.0 },\n                uPulsateSpeed: { value: pulsateSpeed },\n            },\n            vertexShader: `\n        uniform float uTime;\n        uniform float uSize;\n        uniform float uPulsate;\n        uniform float uPulsateSpeed;\n        attribute float scale;\n        varying vec3 vColor;\n        \n        void main() {\n          vColor = color;\n          vec4 modelPosition = modelMatrix * vec4(position, 1.0);\n          \n          // Pulsate particles if enabled\n          if (uPulsate > 0.5) {\n            float pulsateValue = sin(uTime * uPulsateSpeed + position.x * 10.0) * 0.5 + 0.5;\n            modelPosition.y += pulsateValue * 0.02;\n          }\n          \n          vec4 viewPosition = viewMatrix * modelPosition;\n          vec4 projectedPosition = projectionMatrix * viewPosition;\n          gl_Position = projectedPosition;\n          \n          gl_PointSize = uSize * scale * (1.0 / -viewPosition.z);\n        }\n      `,\n            fragmentShader: `\n        varying vec3 vColor;\n        uniform float uGlow;\n        uniform float uDensity;\n        \n        void main() {\n          // Circular particles with customizable glow\n          float distanceToCenter = distance(gl_PointCoord, vec2(0.5));\n          float strength = uGlow / distanceToCenter - (uGlow * 2.0);\n          strength = max(0.0, strength);\n          \n          gl_FragColor = vec4(vColor, strength * uDensity);\n        }\n      `,\n            transparent: true,\n            blending:\n                blendMode === \"additive\" ? THREE.AdditiveBlending : THREE.NormalBlending,\n            depthWrite: false,\n            vertexColors: true,\n        })\n\n            const particles = new THREE.Points(geometry, material)\n            scene.add(particles)\n            particlesRef.current = particles\n\n            // ===== 3D INTERACTION CONTROLS =====\n\n            // Mouse hover for subtle tilt (only when not dragging)\n            const handleMouseMove = (event: MouseEvent) => {\n            if (isDraggingRef.current) return\n            const rect = container.getBoundingClientRect()\n            mouseRef.current.x = ((event.clientX - rect.left) / rect.width) * 2 - 1\n            mouseRef.current.y = -(((event.clientY - rect.top) / rect.height) * 2 - 1)\n        }\n\n            const handleMouseLeave = () => {\n            if (!isDraggingRef.current) {\n                mouseRef.current.x = 0\n                mouseRef.current.y = 0\n            }\n        }\n\n            // Drag to rotate\n            const handleMouseDown = (event: MouseEvent) => {\n            if (!enableDrag) return\n            isDraggingRef.current = true\n            previousMouseRef.current = { x: event.clientX, y: event.clientY }\n            container.style.cursor = \"grabbing\"\n        }\n\n            const handleMouseMoveGlobal = (event: MouseEvent) => {\n            if (!isDraggingRef.current || !enableDrag) return\n\n            const deltaX = event.clientX - previousMouseRef.current.x\n            const deltaY = event.clientY - previousMouseRef.current.y\n\n            targetRotationRef.current.y += deltaX * 0.005\n            targetRotationRef.current.x += deltaY * 0.005\n\n            // Clamp vertical rotation\n            targetRotationRef.current.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, targetRotationRef.current.x))\n\n            previousMouseRef.current = { x: event.clientX, y: event.clientY }\n        }\n\n            const handleMouseUp = () => {\n            if (!enableDrag) return\n            isDraggingRef.current = false\n            container.style.cursor = enableDrag ? \"grab\" : \"default\"\n        }\n\n            // Mouse wheel zoom\n            const handleWheel = (event: WheelEvent) => {\n            if (!enableZoom) return\n            event.preventDefault()\n\n            const zoomSpeed = 0.001\n            targetZoomRef.current += event.deltaY * zoomSpeed\n            targetZoomRef.current = Math.max(minZoom, Math.min(maxZoom, targetZoomRef.current))\n        }\n\n            // Touch support\n            let touchStartDistance = 0\n            let touchStartZoom = 3\n\n            const getTouchDistance = (touches: TouchList) => {\n            if (touches.length < 2) return 0\n            const dx = touches[0]!.clientX - touches[1]!.clientX\n            const dy = touches[0]!.clientY - touches[1]!.clientY\n            return Math.sqrt(dx * dx + dy * dy)\n        }\n\n            const handleTouchStart = (event: TouchEvent) => {\n            if (!enableTouch) return\n\n            if (event.touches.length === 2) {\n                // Pinch zoom\n                touchStartDistance = getTouchDistance(event.touches)\n                touchStartZoom = targetZoomRef.current\n            } else if (event.touches.length === 1 && enableDrag) {\n                // Single touch drag\n                isDraggingRef.current = true\n                previousMouseRef.current = {\n                    x: event.touches[0]!.clientX,\n                    y: event.touches[0]!.clientY,\n                }\n            }\n        }\n\n            const handleTouchMove = (event: TouchEvent) => {\n            if (!enableTouch) return\n\n            if (event.touches.length === 2 && enableZoom) {\n                // Pinch to zoom\n                event.preventDefault()\n                const currentDistance = getTouchDistance(event.touches)\n                const zoomFactor = touchStartDistance / currentDistance\n                targetZoomRef.current = touchStartZoom * zoomFactor\n                targetZoomRef.current = Math.max(minZoom, Math.min(maxZoom, targetZoomRef.current))\n            } else if (event.touches.length === 1 && isDraggingRef.current && enableDrag) {\n                // Drag to rotate\n                const deltaX = event.touches[0]!.clientX - previousMouseRef.current.x\n                const deltaY = event.touches[0]!.clientY - previousMouseRef.current.y\n\n                targetRotationRef.current.y += deltaX * 0.005\n                targetRotationRef.current.x += deltaY * 0.005\n                targetRotationRef.current.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, targetRotationRef.current.x))\n\n                previousMouseRef.current = {\n                    x: event.touches[0]!.clientX,\n                    y: event.touches[0]!.clientY,\n                }\n            }\n        }\n\n            const handleTouchEnd = () => {\n            if (!enableTouch) return\n            isDraggingRef.current = false\n            touchStartDistance = 0\n        }\n\n            // Add event listeners\n            container.addEventListener(\"mousemove\", handleMouseMove)\n            container.addEventListener(\"mouseleave\", handleMouseLeave)\n            container.addEventListener(\"mousedown\", handleMouseDown)\n            document.addEventListener(\"mousemove\", handleMouseMoveGlobal)\n            document.addEventListener(\"mouseup\", handleMouseUp)\n            container.addEventListener(\"wheel\", handleWheel, { passive: false })\n            container.addEventListener(\"touchstart\", handleTouchStart, { passive: false })\n            container.addEventListener(\"touchmove\", handleTouchMove, { passive: false })\n            container.addEventListener(\"touchend\", handleTouchEnd)\n\n            // Set cursor style\n            container.style.cursor = enableDrag ? \"grab\" : \"default\"\n\n            // Animation\n            const clock = new THREE.Clock()\n            const animate = () => {\n            const elapsedTime = clock.getElapsedTime()\n\n            // Update material uniforms\n            if (material.uniforms && material.uniforms.uTime) {\n                material.uniforms.uTime.value = elapsedTime\n            }\n\n            if (particles) {\n                // Smooth damping for rotation\n                currentRotationRef.current.x += (targetRotationRef.current.x - currentRotationRef.current.x) * damping\n                currentRotationRef.current.y += (targetRotationRef.current.y - currentRotationRef.current.y) * damping\n\n                // Auto-rotate when enabled and not being dragged\n                if (autoRotate && !isDraggingRef.current) {\n                    targetRotationRef.current.y += rotationSpeed\n                }\n\n                // Calculate target tilt based on mouse position\n                const targetTiltX = (!isDraggingRef.current && mouseInfluence > 0) ? mouseRef.current.y * mouseInfluence * 0.3 : 0\n                const targetTiltY = (!isDraggingRef.current && mouseInfluence > 0) ? mouseRef.current.x * mouseInfluence * 0.3 : 0\n\n                // Smoothly interpolate current tilt\n                const tiltDamping = 0.05\n                currentTiltRef.current.x += (targetTiltX - currentTiltRef.current.x) * tiltDamping\n                currentTiltRef.current.y += (targetTiltY - currentTiltRef.current.y) * tiltDamping\n\n                // Apply rotation + tilt\n                particles.rotation.x = currentRotationRef.current.x + currentTiltRef.current.x\n                particles.rotation.y = currentRotationRef.current.y + currentTiltRef.current.y\n            }\n\n            // Smooth zoom damping\n            currentZoomRef.current += (targetZoomRef.current - currentZoomRef.current) * damping\n            camera.position.z = currentZoomRef.current\n\n            // Optional gentle camera movement\n            if (cameraMovement && !isDraggingRef.current) {\n                camera.position.x = Math.sin(elapsedTime * 0.1) * 0.2\n                camera.position.y = Math.cos(elapsedTime * 0.15) * 0.2\n            } else if (isDraggingRef.current) {\n                // Smoothly return camera to center when dragging\n                camera.position.x += (0 - camera.position.x) * 0.1\n                camera.position.y += (0 - camera.position.y) * 0.1\n            }\n\n            camera.lookAt(scene.position)\n\n            renderer.render(scene, camera)\n            frameRef.current = requestAnimationFrame(animate)\n        }\n\n            animate()\n\n            // Cleanup\n            return () => {\n            container.removeEventListener(\"mousemove\", handleMouseMove)\n            container.removeEventListener(\"mouseleave\", handleMouseLeave)\n            container.removeEventListener(\"mousedown\", handleMouseDown)\n            document.removeEventListener(\"mousemove\", handleMouseMoveGlobal)\n            document.removeEventListener(\"mouseup\", handleMouseUp)\n            container.removeEventListener(\"wheel\", handleWheel)\n            container.removeEventListener(\"touchstart\", handleTouchStart)\n            container.removeEventListener(\"touchmove\", handleTouchMove)\n            container.removeEventListener(\"touchend\", handleTouchEnd)\n            if (frameRef.current) {\n                cancelAnimationFrame(frameRef.current)\n            }\n            geometry.dispose()\n            material.dispose()\n            renderer.dispose()\n            }\n        } catch {\n            setHasWebGLError(true)\n            return\n        }\n    }, [\n        hasWebGLError,\n        dimensions,\n        particleCount,\n        particleSize,\n        rotationSpeed,\n        spiralArms,\n        colorProp,\n        mouseInfluence,\n        autoRotate,\n        blendMode,\n        spread,\n        density,\n        glow,\n        cameraMovement,\n        centerConcentration,\n        pulsate,\n        pulsateSpeed,\n        enableZoom,\n        enableDrag,\n        enableTouch,\n        damping,\n        minZoom,\n        maxZoom,\n    ])\n\n    if (hasWebGLError) {\n        return (\n            <div ref={containerRef} className={cn(\"relative w-full h-full\", className)}>\n                <WebGLFallback className=\"absolute inset-0 h-full w-full\" />\n            </div>\n        )\n    }\n\n    return (\n        <WebGLErrorBoundary fallback={<WebGLFallback className=\"absolute inset-0 h-full w-full\" />}>\n            <div ref={containerRef} className={cn(\"relative w-full h-full\", className)}>\n                <canvas ref={canvasRef} className=\"w-full h-full\" />\n            </div>\n        </WebGLErrorBoundary>\n    )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "components/ui/webgl-error-boundary.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\ninterface WebGLErrorBoundaryProps {\n  children: React.ReactNode;\n  fallback?: React.ReactNode;\n  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;\n}\n\ninterface WebGLErrorBoundaryState {\n  hasError: boolean;\n}\n\nexport class WebGLErrorBoundary extends React.Component<\n  WebGLErrorBoundaryProps,\n  WebGLErrorBoundaryState\n> {\n  public state: WebGLErrorBoundaryState = { hasError: false };\n\n  static getDerivedStateFromError(): WebGLErrorBoundaryState {\n    return { hasError: true };\n  }\n\n  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {\n    this.props.onError?.(error, errorInfo);\n  }\n\n  render() {\n    if (this.state.hasError) {\n      return this.props.fallback ?? <WebGLFallback />;\n    }\n    return this.props.children;\n  }\n}\n\ninterface WebGLFallbackProps {\n  className?: string;\n  message?: string;\n}\n\nexport function WebGLFallback({\n  className,\n  message = \"Interactive WebGL content is unavailable on this device/browser.\",\n}: WebGLFallbackProps) {\n  return (\n    <div\n      className={cn(\n        \"flex h-full w-full items-center justify-center bg-gradient-to-br from-zinc-950 via-slate-900 to-zinc-900 px-4 text-center text-sm text-white/75\",\n        className,\n      )}\n      role=\"status\"\n      aria-live=\"polite\"\n    >\n      <p>{message}</p>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "title": "Particle Galaxy",
  "devDependencies": [],
  "registryDependencies": []
}