{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "BlurText-JS-TW", "type": "registry:block", "title": "BlurText", "description": "Text starts blurred then crisply resolves for a soft-focus reveal effect.", "dependencies": [ "motion" ], "files": [ { "path": "public/tailwind/src/tailwind/TextAnimations/BlurText/BlurText.jsx", "content": "import { motion } from 'motion/react';\r\nimport { useEffect, useRef, useState, useMemo } from 'react';\r\n\r\nconst buildKeyframes = (from, steps) => {\r\n const keys = new Set([...Object.keys(from), ...steps.flatMap(s => Object.keys(s))]);\r\n\r\n const keyframes = {};\r\n keys.forEach(k => {\r\n keyframes[k] = [from[k], ...steps.map(s => s[k])];\r\n });\r\n return keyframes;\r\n};\r\n\r\nconst BlurText = ({\r\n text = '',\r\n delay = 200,\r\n className = '',\r\n animateBy = 'words',\r\n direction = 'top',\r\n threshold = 0.1,\r\n rootMargin = '0px',\r\n animationFrom,\r\n animationTo,\r\n easing = t => t,\r\n onAnimationComplete,\r\n stepDuration = 0.35\r\n}) => {\r\n const elements = animateBy === 'words' ? text.split(' ') : text.split('');\r\n const [inView, setInView] = useState(false);\r\n const ref = useRef(null);\r\n\r\n useEffect(() => {\r\n if (!ref.current) return;\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting) {\r\n setInView(true);\r\n observer.unobserve(ref.current);\r\n }\r\n },\r\n { threshold, rootMargin }\r\n );\r\n observer.observe(ref.current);\r\n return () => observer.disconnect();\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [threshold, rootMargin]);\r\n\r\n const defaultFrom = useMemo(\r\n () =>\r\n direction === 'top' ? { filter: 'blur(10px)', opacity: 0, y: -50 } : { filter: 'blur(10px)', opacity: 0, y: 50 },\r\n [direction]\r\n );\r\n\r\n const defaultTo = useMemo(\r\n () => [\r\n {\r\n filter: 'blur(5px)',\r\n opacity: 0.5,\r\n y: direction === 'top' ? 5 : -5\r\n },\r\n { filter: 'blur(0px)', opacity: 1, y: 0 }\r\n ],\r\n [direction]\r\n );\r\n\r\n const fromSnapshot = animationFrom ?? defaultFrom;\r\n const toSnapshots = animationTo ?? defaultTo;\r\n\r\n const stepCount = toSnapshots.length + 1;\r\n const totalDuration = stepDuration * (stepCount - 1);\r\n const times = Array.from({ length: stepCount }, (_, i) => (stepCount === 1 ? 0 : i / (stepCount - 1)));\r\n\r\n return (\r\n
\r\n {elements.map((segment, index) => {\r\n const animateKeyframes = buildKeyframes(fromSnapshot, toSnapshots);\r\n\r\n const spanTransition = {\r\n duration: totalDuration,\r\n times,\r\n delay: (index * delay) / 1000\r\n };\r\n spanTransition.ease = easing;\r\n\r\n return (\r\n