{ "name": "letter-cascade", "type": "registry:ui", "dependencies": [], "registryDependencies": [], "description": "Component for letter-cascade", "files": [ { "path": "components/ui/letter-cascade.tsx", "content": "\"use client\";\n\nimport { cn } from \"@workspace/ui/lib/utils\";\nimport {\n type AnimationOptions,\n motion,\n stagger,\n useAnimate,\n} from \"framer-motion\";\nimport { useCallback, useState } from \"react\";\n\ninterface LetterCascadeProps {\n /** The text to animate */\n text: string;\n /** Additional CSS classes for the container */\n className?: string;\n /** CSS classes applied to each individual letter */\n letterClassName?: string;\n /** Stagger delay between each letter in seconds */\n staggerDuration?: number;\n /** Where the stagger wave originates */\n staggerFrom?: \"first\" | \"last\" | \"center\" | number;\n /** Spring stiffness — higher = snappier */\n stiffness?: number;\n /** Spring damping — lower = bouncier */\n damping?: number;\n /** Trigger the animation on click instead of hover */\n triggerOnClick?: boolean;\n /** Callback when the full animation cycle completes */\n onComplete?: () => void;\n}\n\nexport function LetterCascade({\n text,\n className,\n letterClassName,\n staggerDuration = 0.04,\n staggerFrom = \"first\",\n stiffness = 220,\n damping = 16,\n triggerOnClick = false,\n onComplete,\n}: LetterCascadeProps) {\n const [scope, animate] = useAnimate();\n const [blocked, setBlocked] = useState(false);\n\n const trigger = useCallback(() => {\n if (blocked) return;\n setBlocked(true);\n\n const merge = (base: AnimationOptions): AnimationOptions => ({\n ...base,\n delay: stagger(staggerDuration, { from: staggerFrom }),\n });\n\n const spring: AnimationOptions = {\n type: \"spring\",\n stiffness,\n damping,\n };\n\n // ── Phase 1: front tilts back, echo flips in from below ──\n animate(\n \".cascade-front\",\n {\n rotateX: 90,\n opacity: 0,\n y: -6,\n filter: \"blur(4px)\",\n },\n merge(spring)\n ).then(() => {\n // Instantly reset front\n animate(\n \".cascade-front\",\n { rotateX: 0, opacity: 1, y: 0, filter: \"blur(0px)\" },\n { duration: 0 }\n ).then(() => {\n setBlocked(false);\n onComplete?.();\n });\n });\n\n animate(\n \".cascade-echo\",\n {\n rotateX: 0,\n opacity: 1,\n y: 0,\n scale: 1,\n filter: \"blur(0px)\",\n },\n merge(spring)\n ).then(() => {\n // Instantly reset echo\n animate(\n \".cascade-echo\",\n {\n rotateX: -90,\n opacity: 0,\n y: 6,\n scale: 0.8,\n filter: \"blur(4px)\",\n },\n { duration: 0 }\n );\n });\n }, [\n blocked,\n animate,\n staggerDuration,\n staggerFrom,\n stiffness,\n damping,\n onComplete,\n ]);\n\n return (\n \n {text.split(\"\").map((letter, i) => (\n \n {/* Front face — visible by default, tilts backward on trigger */}\n \n {letter}\n \n\n {/* Echo face — hidden below, flips up into view on trigger */}\n \n {letter}\n \n \n ))}\n \n );\n}\n", "type": "registry:ui" } ] }