{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"text-animation","type":"registry:ui","title":"Text Animation","description":"Four text effects: a word-by-word blur reveal with a volt underline, a shimmer sweep, a glyph scramble and a rotating word swap.","author":"GetUrUI <hello@geturui.dev>","dependencies":["motion"],"registryDependencies":[],"files":[{"path":"registry/geturui/ui/text-animation.tsx","type":"registry:ui","target":"components/ui/text-animation.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useInView } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\ntype Tag = \"h1\" | \"h2\" | \"h3\" | \"p\" | \"span\" | \"div\";\n\nexport interface RevealTextProps {\n  text: string;\n  as?: Tag;\n  /** One word to underline with a volt swash once revealed. */\n  mark?: string;\n  /** Seconds between words. */\n  stagger?: number;\n  delay?: number;\n  className?: string;\n}\n\n/** Word-by-word reveal: each word rises and clears a blur, staggered. Runs once when in view. */\nexport function RevealText({ text, as = \"h1\", mark, stagger = 0.06, delay = 0, className }: RevealTextProps) {\n  const ref = React.useRef<HTMLElement>(null);\n  const inView = useInView(ref, { once: true });\n  const Comp = motion[as] as typeof motion.h1;\n  const words = text.split(\" \");\n  return (\n    <Comp ref={ref as React.Ref<HTMLHeadingElement>} className={className} aria-label={text}>\n      {words.map((w, i) => (\n        <React.Fragment key={i}>\n          <motion.span\n            aria-hidden\n            className=\"inline-block\"\n            initial={{ opacity: 0, y: \"0.35em\", filter: \"blur(8px)\" }}\n            animate={inView ? { opacity: 1, y: 0, filter: \"blur(0px)\" } : undefined}\n            transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1], delay: delay + i * stagger }}\n          >\n            {mark === w ? (\n              <span className=\"relative isolate inline-block whitespace-nowrap\">\n                {w}\n                <motion.span\n                  aria-hidden\n                  className=\"absolute -inset-x-[0.06em] -bottom-[0.04em] -z-10 h-[0.16em] origin-left rounded-sm bg-volt\"\n                  initial={{ scaleX: 0 }}\n                  animate={inView ? { scaleX: 1 } : undefined}\n                  transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1], delay: delay + words.length * stagger + 0.15 }}\n                />\n              </span>\n            ) : (\n              w\n            )}\n          </motion.span>\n          {i < words.length - 1 ? \" \" : null}\n        </React.Fragment>\n      ))}\n    </Comp>\n  );\n}\n\n/** Loading text with a light sweep across it. */\nexport function ShimmerText({ children, className }: { children: React.ReactNode; className?: string }) {\n  return <span className={cn(\"shimmer-text\", className)}>{children}</span>;\n}\n\nconst GLYPHS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&*+<>\";\n\n/** Characters scramble through random glyphs before settling, left to right. Re-runs when `text` changes. */\nexport function ScrambleText({ text, duration = 900, className, as: Tag = \"span\" }: { text: string; duration?: number; className?: string; as?: Tag }) {\n  const [out, setOut] = React.useState(text);\n  React.useEffect(() => {\n    const start = performance.now();\n    let raf = 0;\n    const tick = (now: number) => {\n      const p = Math.min(1, (now - start) / duration);\n      const settled = Math.floor(p * text.length);\n      setOut(\n        text\n          .split(\"\")\n          .map((c, i) => (i < settled || c === \" \" ? c : GLYPHS[Math.floor(Math.random() * GLYPHS.length)]))\n          .join(\"\"),\n      );\n      if (p < 1) raf = requestAnimationFrame(tick);\n    };\n    raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(raf);\n  }, [text, duration]);\n  return (\n    <Tag className={cn(\"tabular-nums\", className)} aria-label={text}>\n      <span aria-hidden>{out}</span>\n    </Tag>\n  );\n}\n\n/** Swaps between phrases with a vertical blur crossfade every `interval` ms. */\nexport function RotatingText({ words, interval = 2200, className }: { words: string[]; interval?: number; className?: string }) {\n  const [i, setI] = React.useState(0);\n  React.useEffect(() => {\n    const t = setInterval(() => setI((n) => (n + 1) % words.length), interval);\n    return () => clearInterval(t);\n  }, [words.length, interval]);\n  return (\n    <span className={cn(\"relative inline-grid\", className)}>\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        <motion.span\n          key={words[i]}\n          initial={{ y: \"60%\", opacity: 0, filter: \"blur(6px)\" }}\n          animate={{ y: 0, opacity: 1, filter: \"blur(0px)\" }}\n          exit={{ y: \"-60%\", opacity: 0, filter: \"blur(6px)\" }}\n          transition={{ type: \"spring\", stiffness: 300, damping: 28 }}\n          className=\"inline-block whitespace-nowrap\"\n        >\n          {words[i]}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n"},{"path":"LICENSE-geturui.md","type":"registry:file","target":"LICENSE-geturui.md","content":"MIT License\n\nCopyright (c) 2026 geturui.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"}],"categories":["components","text","headline","reveal","shimmer","scramble","rotate","hero"],"meta":{"tier":"free","license":"MIT","docs":"https://geturui.com/docs/text-animation"}}