{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "typing-text", "type": "registry:ui", "title": "Typing Text", "description": "A text component that dynamically simulates a typing animation, progressively revealing characters as if typed in real-time.", "dependencies": [ "motion" ], "files": [ { "path": "registry/text/typing/index.tsx", "content": "'use client';\n\nimport * as React from 'react';\nimport { motion, useInView, type UseInViewOptions } from 'motion/react';\n\nimport { cn } from '@/lib/utils';\n\nfunction CursorBlinker({ className }: { className?: string }) {\n return (\n \n );\n}\n\ntype TypingTextProps = Omit, 'children'> & {\n duration?: number;\n delay?: number;\n inView?: boolean;\n inViewMargin?: UseInViewOptions['margin'];\n inViewOnce?: boolean;\n cursor?: boolean;\n loop?: boolean;\n holdDelay?: number;\n text: string | string[];\n cursorClassName?: string;\n animateOnChange?: boolean;\n};\n\nfunction TypingText({\n ref,\n duration = 100,\n delay = 0,\n inView = false,\n inViewMargin = '0px',\n inViewOnce = true,\n cursor = false,\n loop = false,\n holdDelay = 1000,\n text,\n cursorClassName,\n animateOnChange = true,\n ...props\n}: TypingTextProps) {\n const localRef = React.useRef(null);\n React.useImperativeHandle(ref, () => localRef.current as HTMLSpanElement);\n\n const inViewResult = useInView(localRef, {\n once: inViewOnce,\n margin: inViewMargin,\n });\n const isInView = !inView || inViewResult;\n\n const [started, setStarted] = React.useState(false);\n const [displayedText, setDisplayedText] = React.useState('');\n\n React.useEffect(() => {\n // Reset animation when text changes (if animateOnChange is true)\n if (animateOnChange) {\n setStarted(false);\n setDisplayedText('');\n }\n\n if (isInView) {\n const timeoutId = setTimeout(() => {\n setStarted(true);\n }, delay);\n return () => clearTimeout(timeoutId);\n } else {\n const timeoutId = setTimeout(() => {\n setStarted(true);\n }, delay);\n return () => clearTimeout(timeoutId);\n }\n }, [isInView, delay, ...(animateOnChange ? [text] : [])]);\n\n React.useEffect(() => {\n if (!started) return;\n const timeoutIds: Array> = [];\n const texts: string[] = typeof text === 'string' ? [text] : text;\n\n const typeText = (str: string, onComplete: () => void) => {\n let currentIndex = 0;\n const type = () => {\n if (currentIndex <= str.length) {\n setDisplayedText(str.substring(0, currentIndex));\n currentIndex++;\n const id = setTimeout(type, duration);\n timeoutIds.push(id);\n } else {\n onComplete();\n }\n };\n type();\n };\n\n const eraseText = (str: string, onComplete: () => void) => {\n let currentIndex = str.length;\n const erase = () => {\n if (currentIndex >= 0) {\n setDisplayedText(str.substring(0, currentIndex));\n currentIndex--;\n const id = setTimeout(erase, duration);\n timeoutIds.push(id);\n } else {\n onComplete();\n }\n };\n erase();\n };\n\n const animateTexts = (index: number) => {\n typeText(texts[index] ?? '', () => {\n const isLast = index === texts.length - 1;\n if (isLast && !loop) {\n return;\n }\n const id = setTimeout(() => {\n eraseText(texts[index] ?? '', () => {\n const nextIndex = isLast ? 0 : index + 1;\n animateTexts(nextIndex);\n });\n }, holdDelay);\n timeoutIds.push(id);\n });\n };\n\n animateTexts(0);\n\n return () => {\n timeoutIds.forEach(clearTimeout);\n };\n }, [text, duration, started, loop, holdDelay]);\n\n return (\n \n {displayedText}\n {cursor && }\n \n );\n}\n\nexport { TypingText, type TypingTextProps };\n", "type": "registry:ui", "target": "components/animate-ui/text/typing.tsx" } ] }