{
  "name": "cursor-driven-particle-typography",
  "type": "registry:ui",
  "dependencies": [],
  "registryDependencies": [],
  "description": "Component for cursor-driven-particle-typography",
  "files": [
    {
      "path": "components/ui/cursor-driven-particle-typography.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@workspace/ui/lib/utils\";\nimport React, { useEffect, useRef } from \"react\";\n\nexport interface CursorDrivenParticleTypographyProps {\n    /** Additional CSS classes */\n    className?: string;\n    /** The text to render */\n    text: string;\n    /** Font size in pixels */\n    fontSize?: number;\n    /** Font family */\n    fontFamily?: string;\n    /** Size of each particle */\n    particleSize?: number;\n    /** Density of particles (lower number = more particles, minimum 1) */\n    particleDensity?: number;\n    /** How strongly the cursor pushes particles away */\n    dispersionStrength?: number;\n    /** Speed at which particles return to origin */\n    returnSpeed?: number;\n    /** Custom color for particles. Overrides inherited text color if set. */\n    color?: string;\n}\n\nclass Particle {\n    x: number;\n    y: number;\n    originX: number;\n    originY: number;\n    vx: number;\n    vy: number;\n    size: number;\n    color: string;\n    dispersion: number;\n    returnSpd: number;\n\n    constructor(\n        x: number,\n        y: number,\n        size: number,\n        color: string,\n        dispersion: number,\n        returnSpd: number\n    ) {\n        this.x = x + (Math.random() - 0.5) * 10; // start with slight randomness\n        this.y = y + (Math.random() - 0.5) * 10;\n        this.originX = x;\n        this.originY = y;\n        this.vx = (Math.random() - 0.5) * 5;\n        this.vy = (Math.random() - 0.5) * 5;\n        this.size = size;\n        this.color = color;\n        this.dispersion = dispersion;\n        this.returnSpd = returnSpd;\n    }\n\n    update(mouseX: number, mouseY: number) {\n        const dx = mouseX - this.x;\n        const dy = mouseY - this.y;\n        const distance = Math.sqrt(dx * dx + dy * dy);\n\n        // Physics interaction with mouse\n        const interactionRadius = 120; // 120px interaction radius\n\n        if (distance < interactionRadius && mouseX !== -1000 && mouseY !== -1000) {\n            const forceDirectionX = dx / distance;\n            const forceDirectionY = dy / distance;\n\n            const force = (interactionRadius - distance) / interactionRadius;\n\n            // Calculate repulsion\n            const repulsionX = forceDirectionX * force * this.dispersion;\n            const repulsionY = forceDirectionY * force * this.dispersion;\n\n            this.vx -= repulsionX;\n            this.vy -= repulsionY;\n        }\n\n        // Return to origin (spring physics)\n        this.vx += (this.originX - this.x) * this.returnSpd;\n        this.vy += (this.originY - this.y) * this.returnSpd;\n\n        // Friction\n        this.vx *= 0.85;\n        this.vy *= 0.85;\n\n        // Add subtle noise/jitter when close to origin\n        const distToOrigin = Math.sqrt(\n            Math.pow(this.x - this.originX, 2) + Math.pow(this.y - this.originY, 2)\n        );\n        if (distToOrigin < 1 && Math.random() > 0.95) {\n            this.vx += (Math.random() - 0.5) * 0.2;\n            this.vy += (Math.random() - 0.5) * 0.2;\n        }\n\n        this.x += this.vx;\n        this.y += this.vy;\n    }\n\n    draw(ctx: CanvasRenderingContext2D) {\n        ctx.fillStyle = this.color;\n        ctx.beginPath();\n        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);\n        ctx.fill();\n    }\n}\n\nexport function CursorDrivenParticleTypography({\n    className,\n    text,\n    fontSize = 120,\n    fontFamily = \"Inter, sans-serif\",\n    particleSize = 1.5,\n    particleDensity = 6,\n    dispersionStrength = 15,\n    returnSpeed = 0.08,\n    color,\n}: CursorDrivenParticleTypographyProps) {\n    const canvasRef = useRef<HTMLCanvasElement>(null);\n    const containerRef = useRef<HTMLDivElement>(null);\n\n    useEffect(() => {\n        const canvas = canvasRef.current;\n        if (!canvas) return;\n        const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n        if (!ctx) return;\n\n        let animationFrameId: number;\n        let particles: Particle[] = [];\n\n        let mouseX = -1000;\n        let mouseY = -1000;\n\n        let containerWidth = 0;\n        let containerHeight = 0;\n\n        const init = () => {\n            const container = containerRef.current;\n            if (!container) return;\n\n            containerWidth = container.clientWidth;\n            containerHeight = container.clientHeight;\n\n            const dpr = window.devicePixelRatio || 1;\n            canvas.width = containerWidth * dpr;\n            canvas.height = containerHeight * dpr;\n            canvas.style.width = `${containerWidth}px`;\n            canvas.style.height = `${containerHeight}px`;\n\n            ctx.scale(dpr, dpr);\n\n            // Determine text color\n            const computedStyle = window.getComputedStyle(container);\n            const textColor = color || computedStyle.color || \"#000000\";\n\n            ctx.clearRect(0, 0, containerWidth, containerHeight);\n\n            // Draw text to generate pixel map\n            ctx.fillStyle = textColor;\n            // Responsive font size based on container width if text is large\n            const effectiveFontSize = Math.min(fontSize, containerWidth * 0.15);\n            ctx.font = `bold ${effectiveFontSize}px ${fontFamily}`;\n            ctx.textAlign = \"center\";\n            ctx.textBaseline = \"middle\";\n\n            // Draw standard text first to measure it\n            ctx.fillText(text, containerWidth / 2, containerHeight / 2);\n\n            // Get pixel data\n            const textCoordinates = ctx.getImageData(0, 0, canvas.width, canvas.height);\n            particles = [];\n\n            // Create particles from text pixels\n            // Step by density multiplied by dpr\n            const step = Math.max(1, Math.floor(particleDensity * dpr));\n            for (let y = 0; y < textCoordinates.height; y += step) {\n                for (let x = 0; x < textCoordinates.width; x += step) {\n                    const index = (y * textCoordinates.width + x) * 4;\n                    const alpha = textCoordinates.data[index + 3] || 0;\n\n                    if (alpha > 128) {\n                        particles.push(\n                            new Particle(\n                                x / dpr,\n                                y / dpr,\n                                particleSize,\n                                textColor,\n                                dispersionStrength,\n                                returnSpeed\n                            )\n                        );\n                    }\n                }\n            }\n        };\n\n        const animate = () => {\n            ctx.clearRect(0, 0, containerWidth, containerHeight);\n\n            particles.forEach((particle) => {\n                particle.update(mouseX, mouseY);\n                particle.draw(ctx);\n            });\n            animationFrameId = requestAnimationFrame(animate);\n        };\n\n        const handleMouseMove = (e: MouseEvent) => {\n            const rect = canvas.getBoundingClientRect();\n            mouseX = e.clientX - rect.left;\n            mouseY = e.clientY - rect.top;\n        };\n\n        const handleMouseLeave = () => {\n            mouseX = -1000;\n            mouseY = -1000;\n        };\n\n        const handleResize = () => {\n            init();\n        };\n\n        // Initialize with a short delay to ensure fonts/layout are ready\n        const timeoutId = setTimeout(() => {\n            init();\n            animate();\n        }, 100);\n\n        const resizeObserver = new ResizeObserver(() => {\n            handleResize();\n        });\n\n        if (containerRef.current) {\n            resizeObserver.observe(containerRef.current);\n        }\n\n        // Re-initialize particles when the theme changes (detects class changes on html tag)\n        const themeObserver = new MutationObserver(() => {\n            init();\n        });\n        themeObserver.observe(document.documentElement, {\n            attributes: true,\n            attributeFilter: [\"class\"]\n        });\n\n        canvas.addEventListener(\"mousemove\", handleMouseMove);\n        canvas.addEventListener(\"mouseleave\", handleMouseLeave);\n        canvas.addEventListener(\"touchstart\", (e) => {\n            if (!e.touches[0]) return;\n            const rect = canvas.getBoundingClientRect();\n            mouseX = e.touches[0].clientX - rect.left;\n            mouseY = e.touches[0].clientY - rect.top;\n        });\n        canvas.addEventListener(\"touchmove\", (e) => {\n            if (!e.touches[0]) return;\n            const rect = canvas.getBoundingClientRect();\n            mouseX = e.touches[0].clientX - rect.left;\n            mouseY = e.touches[0].clientY - rect.top;\n        });\n        canvas.addEventListener(\"touchend\", handleMouseLeave);\n\n        return () => {\n            clearTimeout(timeoutId);\n            resizeObserver.disconnect();\n            themeObserver.disconnect();\n            canvas.removeEventListener(\"mousemove\", handleMouseMove);\n            canvas.removeEventListener(\"mouseleave\", handleMouseLeave);\n            cancelAnimationFrame(animationFrameId);\n        };\n    }, [text, fontSize, fontFamily, particleSize, particleDensity, dispersionStrength, returnSpeed, color]);\n\n    return (\n        <div\n            ref={containerRef}\n            className={cn(\"w-full h-full min-h-[400px] flex items-center justify-center relative touch-none\", className)}\n        >\n            <canvas\n                ref={canvasRef}\n                className=\"block w-full h-full\"\n            />\n        </div>\n    );\n}\n",
      "type": "registry:ui"
    }
  ]
}