{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "BlurText-TS-CSS", "type": "registry:block", "title": "BlurText", "description": "Text starts blurred then crisply resolves for a soft-focus reveal effect.", "dependencies": [ "motion" ], "files": [ { "path": "public/ts/default/src/ts-default/TextAnimations/BlurText/BlurText.tsx", "content": "import { motion, Transition } from 'motion/react';\nimport { useEffect, useRef, useState, useMemo } from 'react';\n\ntype BlurTextProps = {\n text?: string;\n delay?: number;\n className?: string;\n animateBy?: 'words' | 'letters';\n direction?: 'top' | 'bottom';\n threshold?: number;\n rootMargin?: string;\n animationFrom?: Record;\n animationTo?: Array>;\n easing?: (t: number) => number;\n onAnimationComplete?: () => void;\n stepDuration?: number;\n};\n\nconst buildKeyframes = (\n from: Record,\n steps: Array>\n): Record> => {\n const keys = new Set([...Object.keys(from), ...steps.flatMap(s => Object.keys(s))]);\n\n const keyframes: Record> = {};\n keys.forEach(k => {\n keyframes[k] = [from[k], ...steps.map(s => s[k])];\n });\n return keyframes;\n};\n\nconst BlurText: React.FC = ({\n text = '',\n delay = 200,\n className = '',\n animateBy = 'words',\n direction = 'top',\n threshold = 0.1,\n rootMargin = '0px',\n animationFrom,\n animationTo,\n easing = (t: number) => t,\n onAnimationComplete,\n stepDuration = 0.35\n}) => {\n const elements = animateBy === 'words' ? text.split(' ') : text.split('');\n const [inView, setInView] = useState(false);\n const ref = useRef(null);\n\n useEffect(() => {\n if (!ref.current) return;\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setInView(true);\n observer.unobserve(ref.current as Element);\n }\n },\n { threshold, rootMargin }\n );\n observer.observe(ref.current);\n return () => observer.disconnect();\n }, [threshold, rootMargin]);\n\n const defaultFrom = useMemo(\n () =>\n direction === 'top' ? { filter: 'blur(10px)', opacity: 0, y: -50 } : { filter: 'blur(10px)', opacity: 0, y: 50 },\n [direction]\n );\n\n const defaultTo = useMemo(\n () => [\n {\n filter: 'blur(5px)',\n opacity: 0.5,\n y: direction === 'top' ? 5 : -5\n },\n { filter: 'blur(0px)', opacity: 1, y: 0 }\n ],\n [direction]\n );\n\n const fromSnapshot = animationFrom ?? defaultFrom;\n const toSnapshots = animationTo ?? defaultTo;\n\n const stepCount = toSnapshots.length + 1;\n const totalDuration = stepDuration * (stepCount - 1);\n const times = Array.from({ length: stepCount }, (_, i) => (stepCount === 1 ? 0 : i / (stepCount - 1)));\n\n return (\n

\n {elements.map((segment, index) => {\n const animateKeyframes = buildKeyframes(fromSnapshot, toSnapshots);\n\n const spanTransition: Transition = {\n duration: totalDuration,\n times,\n delay: (index * delay) / 1000,\n ease: easing\n };\n\n return (\n \n {segment === ' ' ? '\\u00A0' : segment}\n {animateBy === 'words' && index < elements.length - 1 && '\\u00A0'}\n \n );\n })}\n

\n );\n};\n\nexport default BlurText;\n", "type": "registry:component" } ] }