{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-animate",
  "title": "Text Animate",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Reveals text by character, word or line with eight entrances (fade, blur, slide, scale, rotate), staggered and triggered on scroll, with the full text kept for screen readers.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/text-animate.tsx",
      "content": "// Ballmac UI: Text Animate. https://ui.ballmac.com/components/text-animate\n\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useInView, useReducedMotion, type Variants } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype TextAnimation = \"fadeIn\" | \"blurIn\" | \"slideUp\" | \"slideDown\" | \"slideLeft\" | \"slideRight\" | \"scaleUp\" | \"rotateIn\"\n\nconst PRESETS: Record<TextAnimation, { hidden: Record<string, number | string>; visible: Record<string, number | string> }> = {\n  fadeIn: { hidden: { opacity: 0 }, visible: { opacity: 1 } },\n  blurIn: { hidden: { opacity: 0, filter: \"blur(10px)\" }, visible: { opacity: 1, filter: \"blur(0px)\" } },\n  slideUp: { hidden: { opacity: 0, y: \"0.6em\" }, visible: { opacity: 1, y: 0 } },\n  slideDown: { hidden: { opacity: 0, y: \"-0.6em\" }, visible: { opacity: 1, y: 0 } },\n  slideLeft: { hidden: { opacity: 0, x: \"0.6em\" }, visible: { opacity: 1, x: 0 } },\n  slideRight: { hidden: { opacity: 0, x: \"-0.6em\" }, visible: { opacity: 1, x: 0 } },\n  scaleUp: { hidden: { opacity: 0, scale: 0.4 }, visible: { opacity: 1, scale: 1 } },\n  rotateIn: { hidden: { opacity: 0, rotateX: -90, y: \"0.4em\" }, visible: { opacity: 1, rotateX: 0, y: 0 } },\n}\n\ntype TextAnimateTag = \"span\" | \"p\" | \"div\" | \"h1\" | \"h2\" | \"h3\" | \"h4\"\n\ntype TextAnimateProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** The text. Use a newline to break lines. */\n  children: string\n  /** What is animated one after another. */\n  by?: \"character\" | \"word\" | \"line\"\n  /** How each piece arrives. */\n  animation?: TextAnimation\n  /** Seconds between pieces. Defaults to a pace that suits `by`. */\n  stagger?: number\n  /** Seconds each piece takes. */\n  duration?: number\n  /** Seconds before the first piece. */\n  delay?: number\n  /** Wait for the text to scroll into view. */\n  inView?: boolean\n  /** Play once. */\n  once?: boolean\n  /** Element to render. */\n  as?: TextAnimateTag\n}\n\nfunction TextAnimate({\n  children: text,\n  by = \"word\",\n  animation = \"blurIn\",\n  stagger,\n  duration = 0.4,\n  delay = 0,\n  inView = true,\n  once = true,\n  as = \"span\",\n  className,\n  ...props\n}: TextAnimateProps) {\n  const ref = React.useRef<HTMLElement>(null)\n  const reduce = useReducedMotion()\n  const seen = useInView(ref, { once, margin: \"0px 0px -10% 0px\" })\n  const show = reduce || (inView ? seen : true)\n  const step = stagger ?? (by === \"character\" ? 0.03 : by === \"word\" ? 0.07 : 0.15)\n  const preset = PRESETS[animation]\n  const Tag = as as React.ElementType\n\n  const variants: Variants = {\n    hidden: preset.hidden,\n    visible: (i: number) => ({\n      ...preset.visible,\n      transition: { delay: delay + i * step, duration, ease: [0.22, 1, 0.36, 1] },\n    }),\n  }\n\n  const lines = text.split(\"\\n\")\n  let index = 0\n  const piece = (content: string, key: React.Key) => (\n    <motion.span\n      key={key}\n      custom={index++}\n      variants={variants}\n      initial={false}\n      animate={show ? \"visible\" : \"hidden\"}\n      style={{ display: \"inline-block\", whiteSpace: \"pre\", transformOrigin: \"50% 100%\" }}\n      aria-hidden=\"true\"\n    >\n      {content}\n    </motion.span>\n  )\n\n  return (\n    <Tag ref={ref} data-slot=\"text-animate\" className={cn(\"inline-block\", className)} style={{ perspective: 600 }} {...props}>\n      <span className=\"sr-only\">{text}</span>\n      <span aria-hidden=\"true\">\n        {lines.map((line, li) => (\n          <React.Fragment key={li}>\n            {li > 0 && <br />}\n            {by === \"line\"\n              ? piece(line, li)\n              : line.split(/(\\s+)/).map((word, wi) => {\n                  if (/^\\s+$/.test(word)) return <React.Fragment key={wi}> </React.Fragment>\n                  if (by === \"word\") return piece(word, wi)\n                  return (\n                    <span key={wi} style={{ display: \"inline-block\", whiteSpace: \"nowrap\" }}>\n                      {Array.from(word).map((ch, ci) => piece(ch, ci))}\n                    </span>\n                  )\n                })}\n          </React.Fragment>\n        ))}\n      </span>\n    </Tag>\n  )\n}\n\nexport { TextAnimate, type TextAnimateProps, type TextAnimation }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/text-animate.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "text",
      "animation",
      "reveal",
      "stagger",
      "headline"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "<TextAnimate by='word|character|line' animation='blurIn|fadeIn|slideUp|slideDown|slideLeft|slideRight|scaleUp|rotateIn'>Text</TextAnimate>. Newlines make lines. as picks the element.",
      "whenToUse": [
        "Headlines and short statements that should make an entrance",
        "Section titles revealed on scroll"
      ],
      "whenNotToUse": [
        "Paragraphs of body text",
        "Text that changes often (use word-rotate or typing-text)"
      ],
      "composesWith": [
        "blur-fade",
        "gradient-text",
        "text-reveal"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "The full text is in a hidden element; animated pieces are aria-hidden"
        },
        {
          "keys": "Reduced motion",
          "action": "Text is shown immediately"
        }
      ],
      "customization": [
        "by",
        "animation",
        "stagger and duration",
        "inView and once",
        "as"
      ]
    },
    "examples": [
      "text-animate-demo",
      "text-animate-characters"
    ],
    "url": "https://ui.ballmac.com/components/text-animate"
  },
  "categories": [
    "text"
  ],
  "type": "registry:ui"
}