Fortura/apps/www/public/r/typing-text.json
2025-08-20 04:12:49 -06:00

18 lines
4.7 KiB
JSON

{
"$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 <motion.span\n data-slot=\"cursor-blinker\"\n variants={{\n blinking: {\n opacity: [0, 0, 1, 1],\n transition: {\n duration: 1,\n repeat: Infinity,\n repeatDelay: 0,\n ease: 'linear',\n times: [0, 0.5, 0.5, 1],\n },\n },\n }}\n animate=\"blinking\"\n className={cn(\n 'inline-block h-5 w-[1px] translate-y-1 bg-black dark:bg-white',\n className,\n )}\n />\n );\n}\n\ntype TypingTextProps = Omit<React.ComponentProps<'span'>, '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<HTMLSpanElement>(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<string>('');\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<ReturnType<typeof setTimeout>> = [];\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 <span ref={localRef} data-slot=\"typing-text\" {...props}>\n <motion.span>{displayedText}</motion.span>\n {cursor && <CursorBlinker className={cursorClassName} />}\n </span>\n );\n}\n\nexport { TypingText, type TypingTextProps };\n",
"type": "registry:ui",
"target": "components/animate-ui/text/typing.tsx"
}
]
}