{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pixelated-text","type":"registry:component","title":"Pixelated Text","description":"Text that pixelates in and out.","meta":{"pro":false},"docs":"Usage: https://atelier-ui.com/r/pixelated-text.md\nDocs: https://atelier-ui.com/docs/components/text/pixelated-text","dependencies":[],"registryDependencies":["https://atelier-ui.com/r/agent-rules.json"],"files":[{"path":"registry/pixelated-text/pixelated-text.tsx","type":"registry:component","target":"components/pixelated-text/pixelated-text.tsx","content":"\"use client\"\n\nimport { type ComponentRef, useEffect, useRef } from \"react\"\nimport { useFrameLoop } from \"../../hooks/use-frame-loop\"\nimport { type RenderProp, useRender } from \"../../hooks/use-render\"\n\nexport type PixelatedTextProps = {\n    pixelSize?: number\n    chaos?: number\n    depth?: number\n    colors?: string[]\n    fps?: number\n    children: React.ReactNode\n    render?: RenderProp\n}\n\nfunction randomIndex(length: number, exclude: number) {\n    if (length <= 1) return 0\n    if (exclude < 0) return Math.floor(Math.random() * length)\n    const index = Math.floor(Math.random() * (length - 1))\n    return index >= exclude ? index + 1 : index\n}\n\nfunction drawText(\n    ctx: CanvasRenderingContext2D,\n    textEl: HTMLElement,\n    color: string,\n    width: number,\n    height: number,\n) {\n    const dpr = Math.min(window.devicePixelRatio || 1, 2)\n    const computed = getComputedStyle(textEl)\n\n    ctx.font = `${computed.fontStyle} ${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`\n    ctx.letterSpacing = computed.letterSpacing\n\n    // match browser baseline placement relative to the font em square\n    const metrics = ctx.measureText(textEl.textContent || \"\")\n    const fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent\n    const leading = height - fontHeight\n    const y = leading / 2 + metrics.fontBoundingBoxAscent\n\n    ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n    ctx.clearRect(0, 0, width, height)\n    ctx.fillStyle = color\n    ctx.textBaseline = \"alphabetic\"\n    ctx.fillText(textEl.textContent || \"\", 0, y)\n}\n\nexport function PixelatedText({\n    pixelSize = 7,\n    chaos = 0.2,\n    depth = 8.3,\n    colors,\n    fps = 200,\n    children,\n    render,\n}: PixelatedTextProps) {\n    const sizingRef = useRef<ComponentRef<\"span\">>(null)\n    const containerRef = useRef<ComponentRef<\"span\">>(null)\n    const canvasRef = useRef<ComponentRef<\"canvas\">>(null)\n\n    const bufferRef = useRef<{\n        source: HTMLCanvasElement\n        sourceCtx: CanvasRenderingContext2D\n        shrink: HTMLCanvasElement\n        shrinkCtx: CanvasRenderingContext2D\n    } | null>(null)\n\n    const stateRef = useRef({\n        width: 0,\n        height: 0,\n        pixelWidth: 0,\n        pixelHeight: 0,\n        colorIndex: -1,\n        hasRendered: false,\n    })\n\n    useFrameLoop(() => {\n        const canvas = canvasRef.current\n        const ctx = canvas?.getContext(\"2d\")\n        const textEl = sizingRef.current\n        const buffer = bufferRef.current\n        if (!canvas || !ctx || !textEl || !buffer || !containerRef.current) return\n\n        const state = stateRef.current\n\n        let color: string\n\n        if (colors && colors.length > 0) {\n            const index = randomIndex(colors.length, state.colorIndex)\n            state.colorIndex = index\n            color = colors[index]\n        } else {\n            color = getComputedStyle(containerRef.current).color\n        }\n\n        drawText(buffer.sourceCtx, textEl, color, state.width, state.height)\n\n        const scale = state.pixelHeight / 100\n        const currentPixel = Math.max(\n            2,\n            Math.round((pixelSize + (Math.random() - 0.5) * depth) * scale),\n        )\n\n        const tinyWidth = Math.max(1, Math.ceil(state.pixelWidth / currentPixel))\n        const tinyHeight = Math.max(1, Math.ceil(state.pixelHeight / currentPixel))\n\n        buffer.shrink.width = tinyWidth\n        buffer.shrink.height = tinyHeight\n\n        const gridOffsetX = Math.round((Math.random() - 0.5) * currentPixel * chaos)\n        const gridOffsetY = Math.round((Math.random() - 0.5) * currentPixel * chaos)\n\n        buffer.shrinkCtx.imageSmoothingEnabled = false\n        buffer.shrinkCtx.drawImage(\n            buffer.source,\n            gridOffsetX,\n            gridOffsetY,\n            state.pixelWidth,\n            state.pixelHeight,\n            0,\n            0,\n            tinyWidth,\n            tinyHeight,\n        )\n\n        const dilate = Math.round(currentPixel * chaos * 0.4)\n\n        ctx.clearRect(0, 0, state.pixelWidth, state.pixelHeight)\n        ctx.imageSmoothingEnabled = false\n        ctx.drawImage(\n            buffer.shrink,\n            0,\n            0,\n            tinyWidth,\n            tinyHeight,\n            -dilate,\n            -dilate,\n            state.pixelWidth + dilate * 2,\n            state.pixelHeight + dilate * 2,\n        )\n\n        const randChaos = Math.random() * chaos * 3 - (chaos * 3) / 2\n\n        canvas.style.transform = `translate(${randChaos}px, ${randChaos}px)`\n\n        if (!state.hasRendered) {\n            state.hasRendered = true\n            canvas.style.opacity = \"1\"\n            if (sizingRef.current) sizingRef.current.style.visibility = \"hidden\"\n        }\n    }, fps)\n\n    useEffect(() => {\n        const canvas = canvasRef.current\n        const container = containerRef.current\n        if (!canvas || !container) return\n\n        const source = document.createElement(\"canvas\")\n        const shrink = document.createElement(\"canvas\")\n        const state = stateRef.current\n\n        bufferRef.current = {\n            sourceCtx: source.getContext(\"2d\")!,\n            shrinkCtx: shrink.getContext(\"2d\")!,\n            source,\n            shrink,\n        }\n\n        const measure = () => {\n            const textEl = sizingRef.current\n            if (!textEl) return\n\n            const rect = textEl.getBoundingClientRect()\n            const dpr = Math.min(window.devicePixelRatio || 1, 2)\n\n            if (rect.width === 0 || rect.height === 0) return\n\n            state.width = rect.width\n            state.height = rect.height\n            state.pixelWidth = Math.ceil(rect.width * dpr)\n            state.pixelHeight = Math.ceil(rect.height * dpr)\n\n            source.width = state.pixelWidth\n            source.height = state.pixelHeight\n\n            canvas.width = state.pixelWidth\n            canvas.height = state.pixelHeight\n            canvas.style.width = `${rect.width}px`\n            canvas.style.height = `${rect.height}px`\n        }\n\n        document.fonts.ready.then(measure)\n\n        const resizeObserver = new ResizeObserver(measure)\n        resizeObserver.observe(container)\n\n        return () => {\n            resizeObserver.disconnect()\n            bufferRef.current = null\n            stateRef.current.hasRendered = false\n            if (sizingRef.current) sizingRef.current.style.visibility = \"\"\n        }\n    }, [])\n\n    return useRender({\n        render,\n        defaultElement: <span />,\n        props: {\n            ref: containerRef,\n            className: \"relative inline-block\",\n            children: (\n                <>\n                    <span ref={sizingRef} aria-hidden=\"true\" className=\"inline-block\">\n                        {children}\n                    </span>\n\n                    <span className=\"sr-only\">{children}</span>\n\n                    <canvas\n                        tabIndex={-1}\n                        ref={canvasRef}\n                        className=\"absolute inset-0 pointer-events-none touch-none\"\n                        style={{ opacity: 0 }}\n                        aria-hidden=\"true\"\n                    />\n                </>\n            ),\n        },\n    })\n}\n"},{"path":"registry/hooks/use-frame-loop.ts","type":"registry:hook","target":"hooks/use-frame-loop.ts","content":"import { useEffect, useRef } from \"react\"\n\nconst DELTA_MAX = 0.1\n\ntype FrameLoopCallback = (time: number, delta: number) => void\n\nexport function useFrameLoop(callback: FrameLoopCallback, interval?: number) {\n    const ref = useRef(callback)\n    ref.current = callback\n\n    useEffect(() => {\n        let frameId = 0\n        let lastTime = 0\n        let lastTick = 0\n\n        const tick = (now: number) => {\n            frameId = requestAnimationFrame(tick)\n\n            if (interval && now - lastTick < interval) return\n            if (interval) lastTick = now\n\n            const time = now * 0.001\n            const delta = lastTime ? Math.min(time - lastTime, DELTA_MAX) : 0\n            lastTime = time\n\n            ref.current(time, delta)\n        }\n\n        frameId = requestAnimationFrame(tick)\n\n        return () => {\n            cancelAnimationFrame(frameId)\n        }\n    }, [interval])\n}\n"},{"path":"registry/hooks/use-render.ts","type":"registry:hook","target":"hooks/use-render.ts","content":"// biome-ignore-all lint/suspicious/noExplicitAny: prop merging is inherently dynamic\n/**\n * Inspired by Base UI's `useRender` + `mergeProps`, intentionally simplified for this\n * library's scope at the moment.\n *\n * Chosen over polymorphic prop: cleaner TypeScript, integrates better with other\n * component (Next/Image, design systems, third-party UI libraries)\n *\n * @see https://base-ui.com/react/utils/use-render\n * @see https://base-ui.com/react/utils/merge-props\n */\nimport { cloneElement, isValidElement, type ReactElement, type Ref } from \"react\"\n\ntype AnyProps = Record<string, any>\n\ntype RenderFunction<S> = (props: AnyProps, state: S) => ReactElement\n\nexport type RenderProp<S = void> = ReactElement | RenderFunction<S>\n\ntype UseRenderOptions<S> = {\n    render: RenderProp<S> | undefined\n    props: AnyProps\n    state?: S\n    defaultElement: ReactElement\n}\n\nexport function useRender<S = void>(options: UseRenderOptions<S>): ReactElement<AnyProps> {\n    const { render, props, state, defaultElement } = options\n    const target = render ?? defaultElement\n\n    // Function form: consumer wires props themselves, no merging needed.\n    if (typeof target === \"function\") {\n        return target(props, state as S) as ReactElement<AnyProps>\n    }\n\n    // Element form: clone and merge our internal props with whatever the consumer set on the element.\n    const targetProps = (isValidElement(target) ? target.props : {}) as AnyProps\n    return cloneElement(target, mergeProps(props, targetProps)) as ReactElement<AnyProps>\n}\n\nfunction mergeProps(internal: AnyProps, external: AnyProps): AnyProps {\n    const merged: AnyProps = { ...internal }\n\n    for (const key in external) {\n        const internalValue = internal[key]\n        const externalValue = external[key]\n\n        if (key === \"className\" && typeof externalValue === \"string\") {\n            merged[key] = [internalValue, externalValue].filter(Boolean).join(\" \")\n        } else if (key === \"style\" && externalValue && typeof externalValue === \"object\") {\n            merged[key] = { ...internalValue, ...externalValue }\n        } else if (key === \"ref\") {\n            merged[key] = composeRefs(internalValue, externalValue)\n        } else if (\n            key.startsWith(\"on\") &&\n            typeof internalValue === \"function\" &&\n            typeof externalValue === \"function\"\n        ) {\n            // External handler runs first so consumers can stopPropagation before our logic fires.\n            merged[key] = chainFunctions(externalValue, internalValue)\n        } else {\n            merged[key] = externalValue\n        }\n    }\n\n    return merged\n}\n\nfunction chainFunctions(...fns: Array<(...args: any[]) => void>) {\n    return (...args: any[]) => {\n        for (const fn of fns) fn(...args)\n    }\n}\n\nfunction composeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n    return (node: T) => {\n        for (const ref of refs) {\n            if (typeof ref === \"function\") ref(node)\n            else if (ref != null) (ref as { current: T | null }).current = node\n        }\n    }\n}\n"}]}