{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "BlurText-JS-TW",
	"title": "BlurText",
	"description": "Text starts blurred then crisply resolves for a soft-focus reveal effect.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "BlurText/BlurText.jsx",
			"content": "import { motion } from 'motion/react';\nimport { useEffect, useRef, useState, useMemo } from 'react';\n\nconst buildKeyframes = (from, steps) => {\n  const keys = new Set([...Object.keys(from), ...steps.flatMap(s => Object.keys(s))]);\n\n  const keyframes = {};\n  keys.forEach(k => {\n    keyframes[k] = [from[k], ...steps.map(s => s[k])];\n  });\n  return keyframes;\n};\n\nconst BlurText = ({\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 => 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);\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    <p ref={ref} className={`blur-text ${className} flex flex-wrap`}>\n      {elements.map((segment, index) => {\n        const animateKeyframes = buildKeyframes(fromSnapshot, toSnapshots);\n\n        const spanTransition = {\n          duration: totalDuration,\n          times,\n          delay: (index * delay) / 1000\n        };\n        spanTransition.ease = easing;\n\n        return (\n          <motion.span\n            className=\"inline-block will-change-[transform,filter,opacity]\"\n            key={index}\n            initial={fromSnapshot}\n            animate={inView ? animateKeyframes : fromSnapshot}\n            transition={spanTransition}\n            onAnimationComplete={index === elements.length - 1 ? onAnimationComplete : undefined}\n          >\n            {segment === ' ' ? '\\u00A0' : segment}\n            {animateBy === 'words' && index < elements.length - 1 && '\\u00A0'}\n          </motion.span>\n        );\n      })}\n    </p>\n  );\n};\n\nexport default BlurText;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12"
	]
}