{
  "name": "pixel-canvas",
  "type": "registry:ui",
  "dependencies": [],
  "registryDependencies": [],
  "description": "An interactive pixel grid with smooth trailing effects that lights up on hover and decays over time.",
  "files": [
    {
      "path": "components/ui/pixel-canvas.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef, useCallback } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface PixelCanvasProps extends React.HTMLAttributes<HTMLDivElement> {\n    /** Size of each pixel cell in pixels */\n    gap?: number;\n    /** Speed of the trailing decay (higher = faster fade) */\n    speed?: number;\n    /** Array of colors for pixels - will interpolate through them as trail fades */\n    colors?: string[];\n    /** Disable mouse tracking */\n    noFocus?: boolean;\n    /** Variant style */\n    variant?: \"default\" | \"trail\" | \"glow\";\n}\n\ninterface Pixel {\n    x: number;\n    y: number;\n    size: number;\n    intensity: number;\n    targetIntensity: number;\n    colorPhase: number;\n}\n\n// Helper to interpolate between two hex colors\nfunction lerpColor(color1: string, color2: string, t: number): string {\n    const c1 = hexToRgb(color1);\n    const c2 = hexToRgb(color2);\n    if (!c1 || !c2) return color1;\n\n    const r = Math.round(c1.r + (c2.r - c1.r) * t);\n    const g = Math.round(c1.g + (c2.g - c1.g) * t);\n    const b = Math.round(c1.b + (c2.b - c1.b) * t);\n\n    return `rgb(${r}, ${g}, ${b})`;\n}\n\nfunction hexToRgb(hex: string): { r: number; g: number; b: number } | null {\n    const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n    return result\n        ? {\n            r: parseInt(result[1]!, 16),\n            g: parseInt(result[2]!, 16),\n            b: parseInt(result[3]!, 16),\n        }\n        : null;\n}\n\nexport function PixelCanvas({\n    className,\n    gap = 6,\n    speed = 0.02,\n    colors = [\"#e879f9\", \"#a78bfa\", \"#38bdf8\", \"#22d3ee\"],\n    noFocus = false,\n    variant = \"default\",\n    ...props\n}: PixelCanvasProps) {\n    const canvasRef = useRef<HTMLCanvasElement>(null);\n    const containerRef = useRef<HTMLDivElement>(null);\n    const pixelsRef = useRef<Pixel[][]>([]);\n    const mouseRef = useRef({ x: -1000, y: -1000 });\n    const animationRef = useRef<number>(0);\n    const lastTimeRef = useRef<number>(0);\n\n    const getColorFromIntensity = useCallback((intensity: number, phase: number) => {\n        if (colors.length === 0) return \"#ffffff\";\n        if (colors.length === 1) return colors[0]!;\n\n        // Use phase + intensity to create a shifting color effect\n        const t = (phase + intensity) % 1;\n        const index = Math.floor(t * (colors.length - 1));\n        const nextIndex = Math.min(index + 1, colors.length - 1);\n        const localT = (t * (colors.length - 1)) % 1;\n\n        const color1 = colors[index];\n        const color2 = colors[nextIndex];\n\n        if (!color1) return \"#ffffff\";\n        if (!color2) return color1!;\n\n        return lerpColor(color1, color2, localT);\n    }, [colors]);\n\n    useEffect(() => {\n        const canvas = canvasRef.current;\n        const container = containerRef.current;\n        if (!canvas || !container) return;\n\n        const ctx = canvas.getContext(\"2d\", { alpha: true });\n        if (!ctx) return;\n\n        let cols = 0;\n        let rows = 0;\n        const pixelSize = Math.max(gap, 4);\n\n        const initPixels = () => {\n            const rect = container.getBoundingClientRect();\n            const dpr = window.devicePixelRatio || 1;\n\n            canvas.width = rect.width * dpr;\n            canvas.height = rect.height * dpr;\n            canvas.style.width = `${rect.width}px`;\n            canvas.style.height = `${rect.height}px`;\n            ctx.scale(dpr, dpr);\n\n            cols = Math.ceil(rect.width / pixelSize);\n            rows = Math.ceil(rect.height / pixelSize);\n\n            const newPixels: Pixel[][] = [];\n            for (let i = 0; i < cols; i++) {\n                const row: Pixel[] = [];\n                for (let j = 0; j < rows; j++) {\n                    // Preserve existing intensity if pixel exists\n                    const existing = pixelsRef.current[i]?.[j];\n                    row.push({\n                        x: i * pixelSize,\n                        y: j * pixelSize,\n                        size: pixelSize - 1,\n                        intensity: existing?.intensity ?? 0,\n                        targetIntensity: 0,\n                        colorPhase: Math.random(), // Random starting phase for color variety\n                    });\n                }\n                newPixels.push(row);\n            }\n            pixelsRef.current = newPixels;\n        };\n\n        const draw = (timestamp: number) => {\n            const deltaTime = timestamp - lastTimeRef.current;\n            lastTimeRef.current = timestamp;\n\n            const rect = container.getBoundingClientRect();\n            ctx.clearRect(0, 0, rect.width, rect.height);\n\n            const { x: mouseX, y: mouseY } = mouseRef.current;\n            const pixels = pixelsRef.current;\n\n            // Influence radius based on variant\n            const radius = variant === \"glow\" ? 120 : 80;\n            const glowPasses = variant === \"glow\" ? 2 : 1;\n\n            // Update pixel states\n            for (let i = 0; i < cols; i++) {\n                const col = pixels[i];\n                if (!col) continue;\n\n                for (let j = 0; j < rows; j++) {\n                    const pixel = col[j];\n                    if (!pixel) continue;\n\n                    // Calculate distance from mouse\n                    const centerX = pixel.x + pixel.size / 2;\n                    const centerY = pixel.y + pixel.size / 2;\n                    const dx = mouseX - centerX;\n                    const dy = mouseY - centerY;\n                    const distance = Math.sqrt(dx * dx + dy * dy);\n\n                    // Set target intensity based on distance\n                    if (distance < radius) {\n                        const falloff = 1 - (distance / radius);\n                        // Smooth falloff curve\n                        pixel.targetIntensity = Math.pow(falloff, 1.5);\n                    } else {\n                        pixel.targetIntensity = 0;\n                    }\n\n                    // Smooth interpolation towards target\n                    const lerpSpeed = pixel.targetIntensity > pixel.intensity\n                        ? 0.3 // Quick light up\n                        : speed; // Slow decay for trailing\n\n                    pixel.intensity += (pixel.targetIntensity - pixel.intensity) * lerpSpeed;\n\n                    // Shift color phase slowly for shimmer effect\n                    pixel.colorPhase = (pixel.colorPhase + 0.001 * (deltaTime / 16)) % 1;\n\n                    // Only draw if visible\n                    if (pixel.intensity > 0.01) {\n                        const color = getColorFromIntensity(pixel.intensity, pixel.colorPhase);\n\n                        // Glow effect: draw larger, blurred version first\n                        if (variant === \"glow\" && pixel.intensity > 0.2) {\n                            for (let g = glowPasses; g > 0; g--) {\n                                const glowSize = pixel.size + g * 4;\n                                const glowOffset = (glowSize - pixel.size) / 2;\n                                ctx.globalAlpha = pixel.intensity * 0.15 / g;\n                                ctx.fillStyle = color;\n                                ctx.fillRect(\n                                    pixel.x - glowOffset,\n                                    pixel.y - glowOffset,\n                                    glowSize,\n                                    glowSize\n                                );\n                            }\n                        }\n\n                        // Main pixel\n                        ctx.globalAlpha = pixel.intensity * 0.9;\n                        ctx.fillStyle = color;\n\n                        if (variant === \"trail\") {\n                            // Rounded pixels for trail variant\n                            const cornerRadius = pixel.size * 0.3;\n                            ctx.beginPath();\n                            ctx.roundRect(pixel.x, pixel.y, pixel.size, pixel.size, cornerRadius);\n                            ctx.fill();\n                        } else {\n                            ctx.fillRect(pixel.x, pixel.y, pixel.size, pixel.size);\n                        }\n                    }\n                }\n            }\n\n            ctx.globalAlpha = 1;\n            animationRef.current = requestAnimationFrame(draw);\n        };\n\n        const onMouseMove = (e: MouseEvent) => {\n            const rect = canvas.getBoundingClientRect();\n            mouseRef.current = {\n                x: e.clientX - rect.left,\n                y: e.clientY - rect.top,\n            };\n        };\n\n        const onMouseLeave = () => {\n            mouseRef.current = { x: -1000, y: -1000 };\n        };\n\n        const onTouchMove = (e: TouchEvent) => {\n            if (e.touches.length > 0) {\n                const touch = e.touches[0];\n                if (touch) {\n                    const rect = canvas.getBoundingClientRect();\n                    mouseRef.current = {\n                        x: touch.clientX - rect.left,\n                        y: touch.clientY - rect.top,\n                    };\n                }\n            }\n        };\n\n        const onTouchEnd = () => {\n            mouseRef.current = { x: -1000, y: -1000 };\n        };\n\n        // Initialize\n        initPixels();\n        lastTimeRef.current = performance.now();\n        animationRef.current = requestAnimationFrame(draw);\n\n        // Event listeners\n        window.addEventListener(\"resize\", initPixels);\n        if (!noFocus) {\n            container.addEventListener(\"mousemove\", onMouseMove);\n            container.addEventListener(\"mouseleave\", onMouseLeave);\n            container.addEventListener(\"touchmove\", onTouchMove, { passive: true });\n            container.addEventListener(\"touchend\", onTouchEnd);\n        }\n\n        return () => {\n            cancelAnimationFrame(animationRef.current);\n            window.removeEventListener(\"resize\", initPixels);\n            container.removeEventListener(\"mousemove\", onMouseMove);\n            container.removeEventListener(\"mouseleave\", onMouseLeave);\n            container.removeEventListener(\"touchmove\", onTouchMove);\n            container.removeEventListener(\"touchend\", onTouchEnd);\n        };\n    }, [gap, speed, noFocus, variant, getColorFromIntensity]);\n\n    return (\n        <div\n            ref={containerRef}\n            className={cn(\"h-full w-full relative overflow-hidden\", className)}\n            {...props}\n        >\n            <canvas ref={canvasRef} className=\"block w-full h-full\" />\n        </div>\n    );\n}\n"
    }
  ]
}
