{
    "name": "ai-response-writer",
    "title": "Response Writer",
    "description": "An auto-scrolling text display that smoothly keeps the latest response always visible",
    "type": "registry:ui",
    "dependencies": ["gsap", "@gsap/react"],
    "files": [
        {
            "path": "components/gsap/ai-response-writer.tsx",
            "type": "registry:ui",
            "target": "~/components/gsap/ai-response-writer.tsx",
            "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\n\ntype ResponseWriterProps = {\n    text: string;\n};\n\nexport const AiResponseWriter = ({ text, ...props }: ResponseWriterProps) => {\n    const scrollAreaRef = useRef<HTMLDivElement>(null);\n\n    useEffect(() => {\n        if (scrollAreaRef.current) {\n            const viewport = scrollAreaRef.current?.querySelector(\"div:first-child\");\n            if (viewport) {\n                viewport.scrollTo({\n                    top: viewport.scrollHeight,\n                    behavior: \"smooth\",\n                });\n            }\n        }\n    }, [text]);\n\n    return (\n        <ScrollArea ref={scrollAreaRef} {...props}>\n            <div className=\"pr-4\">\n                <p className=\"text-foreground/80 text-sm whitespace-pre-line\">{text}</p>\n            </div>\n        </ScrollArea>\n    );\n};\n"
        },
        {
            "path": "hooks/gsap/use-writer.ts",
            "type": "registry:hook",
            "target": "~/hooks/gsap/use-writer.ts",
            "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\ntype WriterMode = \"character\" | \"word\";\n\ninterface UseWriterOptions {\n    speed?: number;\n    reverseSpeed?: number;\n    mode?: WriterMode;\n    skipEmptyIntermediate?: boolean;\n    onDone?: () => void;\n}\n\nexport function useWriter(text: string, options: UseWriterOptions = {}) {\n    const { speed = 10, reverseSpeed = 20, mode = \"character\", skipEmptyIntermediate = false, onDone } = options;\n\n    const [displayText, setDisplayText] = useState(\"\");\n    const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n    const clearTimer = () => {\n        if (timerRef.current) {\n            clearTimeout(timerRef.current);\n            timerRef.current = null;\n        }\n    };\n\n    useEffect(() => {\n        let isMounted = true;\n\n        const getUnits = (str: string) => (mode === \"word\" ? str.split(\" \") : str.split(\"\"));\n\n        const currentUnits = getUnits(displayText);\n        const targetUnits = getUnits(text);\n\n        let commonLength = 0;\n        for (let i = 0; i < Math.min(currentUnits.length, targetUnits.length); i++) {\n            if (currentUnits[i] === targetUnits[i]) {\n                commonLength++;\n            } else {\n                break;\n            }\n        }\n\n        const removeStep = () => {\n            if (!isMounted) return;\n\n            if (currentUnits.length > commonLength) {\n                currentUnits.pop();\n\n                if (skipEmptyIntermediate && currentUnits.length === 0) {\n                    typeStep();\n                    return;\n                }\n\n                setDisplayText(mode === \"word\" ? currentUnits.join(\" \") : currentUnits.join(\"\"));\n\n                timerRef.current = setTimeout(removeStep, 1000 / reverseSpeed);\n            } else {\n                typeStep();\n            }\n        };\n\n        const typeStep = () => {\n            if (!isMounted) return;\n\n            if (currentUnits.length < targetUnits.length) {\n                currentUnits.push(targetUnits[currentUnits.length]);\n                setDisplayText(mode === \"word\" ? currentUnits.join(\" \") : currentUnits.join(\"\"));\n                timerRef.current = setTimeout(typeStep, 1000 / speed);\n            } else {\n                onDone?.();\n            }\n        };\n\n        clearTimer();\n        removeStep();\n\n        return () => {\n            isMounted = false;\n            clearTimer();\n        };\n    }, [text, speed, reverseSpeed, mode, skipEmptyIntermediate]);\n\n    return displayText;\n}\n"
        }
    ]
}
