{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "particle-galaxy",
  "type": "registry:block",
  "title": "Particle galaxy",
  "description": "Particle galaxy",
  "files": [
    {
      "path": "components/usages/particlegalaxyusage.tsx",
      "content": "import { ParticleGalaxy } from \"@/registry/open-source/particle-galaxy\"\n\nexport default function Usage() {\n    return (\n        <div className=\"relative h-screen w-full\">\n            <ParticleGalaxy />\n            <div className=\"relative z-10 flex items-center justify-center h-full\">\n                <h1 className=\"text-6xl font-bold\">Your Content Here</h1>\n            </div>\n        </div>\n    )\n}",
      "type": "registry:block",
      "target": "~/example.tsx"
    },
    {
      "path": "components/usages/particlegalaxyusage.tsx",
      "content": "import { ParticleGalaxy } from \"@/registry/open-source/particle-galaxy\"\n\nexport default function Usage() {\n    return (\n        <div className=\"relative h-screen w-full\">\n            <ParticleGalaxy />\n            <div className=\"relative z-10 flex items-center justify-center h-full\">\n                <h1 className=\"text-6xl font-bold\">Your Content Here</h1>\n            </div>\n        </div>\n    )\n}",
      "type": "registry:ui"
    },
    {
      "path": "registry/open-source/particle-galaxy.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/registry/utilities/cn\"\nimport React, { useEffect, useRef, useState } from \"react\"\nimport * as THREE from \"three\"\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}\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": "registry/utilities/cn.ts",
      "content": "import { ClassValue, clsx } from \"clsx\";\r\nimport { twMerge } from \"tailwind-merge\";\r\n\r\nexport function cn(...inputs: ClassValue[]) {\r\n\treturn twMerge(clsx(inputs));\r\n}\r\n",
      "type": "registry:ui"
    }
  ]
}