{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pixel-scroll","type":"registry:component","title":"Pixel Scroll","description":"A grid of pixels that fills in as you scroll.","meta":{"pro":false},"docs":"Usage: https://atelier-ui.com/r/pixel-scroll.md\nDocs: https://atelier-ui.com/docs/components/scroll/pixel-scroll","dependencies":["motion"],"registryDependencies":["https://atelier-ui.com/r/smooth-scroll.json","https://atelier-ui.com/r/agent-rules.json"],"files":[{"path":"registry/pixel-scroll/pixel-scroll.tsx","type":"registry:component","target":"components/pixel-scroll/pixel-scroll.tsx","content":"\"use client\"\n\nimport { useScroll } from \"motion/react\"\nimport { type ComponentRef, useEffect, useMemo, useRef, useState } from \"react\"\n\nconst MAX_COLUMNS = 200\n\ntype Cell = {\n    color: string | null\n    fillAt: number\n    flashAt: number\n}\n\nexport type PixelScrollProps = {\n    pixelSize?: number\n    height?: string\n    colors?: string[]\n    colorRatio?: number\n    randomness?: number\n    direction?: \"cover\" | \"clear\" | \"sweep\"\n    scrollDistance?: number\n    overlap?: number\n    className?: string\n}\n\nfunction cover(cell: Cell, progress: number, settled: string) {\n    if (progress > cell.fillAt) return settled\n    if (cell.color && progress > cell.flashAt) return cell.color\n    return null\n}\n\nfunction clear(cell: Cell, progress: number, settled: string) {\n    if (progress < cell.flashAt) return settled\n    if (cell.color && progress < cell.fillAt) return cell.color\n    return null\n}\n\nfunction sweep(cell: Cell, progress: number, settled: string) {\n    if (progress <= 0.5) return cover(cell, progress * 2, settled)\n    return clear(cell, (progress - 0.5) * 2, settled)\n}\n\nexport default function PixelScroll({\n    pixelSize = 50,\n    height,\n    colors = [],\n    colorRatio = 0.25,\n    randomness = 0.4,\n    direction = \"cover\",\n    scrollDistance = 200,\n    overlap = 0,\n    className,\n}: PixelScrollProps) {\n    const [size, setSize] = useState({ width: 0, height: 0, rows: 0, cols: 0 })\n    const canvasRef = useRef<ComponentRef<\"canvas\">>(null)\n    const ownTargetRef = useRef<ComponentRef<\"section\">>(null)\n    const pinned = height === undefined\n\n    const { scrollYProgress } = useScroll({\n        target: ownTargetRef,\n        offset: pinned ? [\"start start\", \"end end\"] : [\"start end\", \"end start\"],\n    })\n\n    useEffect(() => {\n        const element = canvasRef.current\n        if (!element) return\n\n        const observer = new ResizeObserver(([{ contentRect }]) => {\n            const { width, height } = contentRect\n            if (width && height) {\n                const cols = Math.min(MAX_COLUMNS, Math.max(1, Math.round(width / pixelSize)))\n                const rows = Math.max(1, Math.ceil(height / (width / cols)))\n                setSize({ width, height, rows, cols })\n            }\n        })\n\n        observer.observe(element)\n        return () => observer.disconnect()\n    }, [pixelSize])\n\n    const cells = useMemo(\n        () =>\n            Array.from({ length: size.cols * size.rows }, (_, index) => {\n                const row = Math.floor(index / size.cols)\n                const rise = (size.rows - 1 - row) / Math.max(1, size.rows - 1)\n                const fillAt = Math.min(1, rise * (1 - randomness) + Math.random() * randomness)\n                const flashes = colors.length > 0 && Math.random() < colorRatio\n                const color = flashes ? colors[Math.floor(Math.random() * colors.length)] : null\n\n                return {\n                    color,\n                    fillAt,\n                    flashAt: color ? Math.max(0, fillAt - 0.08) : fillAt,\n                }\n            }),\n        [size.cols, size.rows, colors, colorRatio, randomness],\n    )\n\n    useEffect(() => {\n        function paintOnScroll() {\n            const canvas = canvasRef.current\n            if (!canvas || !size.width || !size.height) return\n            const context = canvas.getContext(\"2d\")\n            if (!context) return\n\n            const { width, height, rows, cols } = size\n            const pixelRatio = window.devicePixelRatio || 1\n            canvas.width = Math.round(width * pixelRatio)\n            canvas.height = Math.round(height * pixelRatio)\n            context.scale(pixelRatio, pixelRatio)\n\n            const cell = width / cols\n\n            let fillOf = cover\n            if (direction === \"clear\") fillOf = clear\n            if (direction === \"sweep\") fillOf = sweep\n\n            let settledColor = getComputedStyle(canvas).color\n\n            const paint = (progress: number) => {\n                context.clearRect(0, 0, width, height)\n                for (let index = 0; index < cells.length; index++) {\n                    const fillColor = fillOf(cells[index], progress, settledColor)\n\n                    if (fillColor) {\n                        const cellX = (index % cols) * cell\n                        const cellY = height - (rows - Math.floor(index / cols)) * cell\n                        context.fillStyle = fillColor\n                        context.fillRect(\n                            Math.floor(cellX),\n                            Math.floor(cellY),\n                            Math.ceil(cell),\n                            Math.ceil(cell),\n                        )\n                    }\n                }\n            }\n\n            paint(scrollYProgress.get())\n            const unsubscribe = scrollYProgress.on(\"change\", paint)\n\n            // Canvas pixels are painted by hand, so unlike CSS they don't restyle when the root's class or data-theme changes. Repaint when they do.\n            const observer = new MutationObserver(() => {\n                settledColor = getComputedStyle(canvas).color\n                paint(scrollYProgress.get())\n            })\n\n            observer.observe(document.documentElement, {\n                attributes: true,\n                attributeFilter: [\"class\", \"data-theme\"],\n            })\n\n            return () => {\n                unsubscribe()\n                observer.disconnect()\n            }\n        }\n\n        return paintOnScroll()\n    }, [size, cells, direction, scrollYProgress])\n\n    // Canvas redraws the whole grid each scroll frame in a single loop, instead of using a div per pixel (for performance reasons)\n    const canvas = <canvas ref={canvasRef} className={`block size-full ${className ?? \"\"}`} />\n\n    return (\n        <section\n            ref={ownTargetRef}\n            className=\"relative\"\n            style={{\n                height: pinned ? `${scrollDistance + 100}vh` : height,\n                margin: `${-overlap / 2}vh 0`,\n            }}\n        >\n            {pinned ? <div className=\"sticky top-0 h-screen\">{canvas}</div> : canvas}\n        </section>\n    )\n}\n"}]}