{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"text-scramble","type":"registry:component","title":"Text Scramble","description":"Text that scrambles into place.","meta":{"pro":false},"docs":"Usage: https://atelier-ui.com/r/text-scramble.md\nDocs: https://atelier-ui.com/docs/components/text/text-scramble","dependencies":[],"registryDependencies":["https://atelier-ui.com/r/agent-rules.json"],"files":[{"path":"registry/text-scramble/text-scramble.tsx","type":"registry:component","target":"components/text-scramble/text-scramble.tsx","content":"\"use client\"\n\nimport { type ComponentRef, useCallback, useEffect, useRef } from \"react\"\nimport { useFrameLoop } from \"../../hooks/use-frame-loop\"\nimport { type RenderProp, useRender } from \"../../hooks/use-render\"\n\nexport type TextScrambleProps = {\n    children: string\n    duration?: number\n    scrambleFps?: number\n    playOnMount?: boolean\n    playOnHover?: boolean\n    characters?: string\n    render?: RenderProp\n}\n\nexport function TextScramble({\n    children,\n    duration = 0.7,\n    scrambleFps = 30,\n    playOnMount = true,\n    playOnHover = true,\n    characters = \"abcdefghijklmnopqrstuvwxyz@!#*$%^&+_[]\",\n    render,\n}: TextScrambleProps) {\n    const text = children\n\n    const ref = useRef<ComponentRef<\"span\">>(null)\n    const startTime = useRef(0)\n    const isAnimating = useRef(false)\n\n    const play = useCallback(() => {\n        startTime.current = 0\n        isAnimating.current = true\n    }, [])\n\n    useEffect(() => {\n        if (playOnMount) play()\n    }, [text, playOnMount, play])\n\n    useFrameLoop((time) => {\n        if (!isAnimating.current) return\n        if (!ref.current) return\n        if (!startTime.current) startTime.current = time\n\n        const elapsed = time - startTime.current\n        const resolved = Math.floor((elapsed / duration) * text.length)\n\n        if (resolved >= text.length) {\n            ref.current.textContent = text\n            isAnimating.current = false\n            return\n        }\n\n        let next = \"\"\n        for (let i = 0; i < text.length; i++) {\n            if (i < resolved) next += text[i]\n            else if (text[i] === \" \") next += \" \"\n            else next += characters[Math.floor(Math.random() * characters.length)]\n        }\n\n        ref.current.textContent = next\n    }, 1000 / scrambleFps)\n\n    return useRender({\n        render,\n        defaultElement: <span />,\n        props: {\n            ref,\n            onTouchStart: playOnHover ? play : undefined,\n            onMouseEnter: playOnHover ? play : undefined,\n            children: text,\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"}]}