{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-animate",
  "title": "Text Animate",
  "description": "Animates text in by word or character with configurable effects.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/text-animate/text-animate.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/** Per-segment entrance animation. */\nexport type TextAnimateAnimation = \"blur\" | \"fade\" | \"slide-up\";\n\n/** Props for {@link TextAnimate}. */\nexport type TextAnimateProps = React.ComponentPropsWithoutRef<\"div\"> & {\n  /** Entrance style. Defaults to `\"fade\"`. */\n  animation?: TextAnimateAnimation;\n  /** Split granularity. Defaults to `\"word\"`. */\n  by?: \"character\" | \"word\";\n  /** Text split into animated segments. */\n  children: string;\n  /** Milliseconds of stagger between segments. Defaults to `60`. */\n  delay?: number;\n  /** Wait until scrolled into view before animating. Defaults to `true`. */\n  startOnView?: boolean;\n};\n\nfunction usePrefersReducedMotion(): boolean {\n  const [reduced, setReduced] = React.useState(false);\n\n  React.useEffect(() => {\n    if (\n      typeof window === \"undefined\" ||\n      typeof window.matchMedia !== \"function\"\n    ) {\n      return;\n    }\n\n    const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const onChange = (): void => {\n      setReduced(query.matches);\n    };\n\n    onChange();\n    query.addEventListener(\"change\", onChange);\n\n    return () => {\n      query.removeEventListener(\"change\", onChange);\n    };\n  }, []);\n\n  return reduced;\n}\n\nfunction splitText(text: string, by: \"character\" | \"word\"): string[] {\n  if (by === \"character\") {\n    return text.match(/[\\s\\S]/gu) ?? [];\n  }\n  return text.split(/(\\s+)/).filter((segment) => segment.length > 0);\n}\n\nfunction useInView(\n  enabled: boolean,\n): [React.RefObject<HTMLDivElement | null>, boolean] {\n  const nodeRef = React.useRef<HTMLDivElement>(null);\n  const [inView, setInView] = React.useState(!enabled);\n\n  React.useEffect(() => {\n    const node = nodeRef.current;\n    if (!enabled || !node || typeof IntersectionObserver !== \"function\") {\n      setInView(true);\n      return;\n    }\n    const observer = new IntersectionObserver((entries) => {\n      if (entries.some((entry) => entry.isIntersecting)) {\n        setInView(true);\n        observer.disconnect();\n      }\n    });\n    observer.observe(node);\n    return () => {\n      observer.disconnect();\n    };\n  }, [enabled]);\n\n  return [nodeRef, inView];\n}\n\nfunction assignRef(\n  ref: React.Ref<HTMLDivElement> | undefined,\n  node: HTMLDivElement | null,\n): void {\n  if (typeof ref === \"function\") {\n    ref(node);\n  } else if (ref) {\n    ref.current = node;\n  }\n}\n\nconst animationClasses: Record<TextAnimateAnimation, string> = {\n  blur: \"[filter:blur(6px)] opacity-0\",\n  fade: \"animate-in fade-in-0\",\n  \"slide-up\": \"animate-in fade-in-0 slide-in-from-bottom-2\",\n};\n\nfunction Segment({\n  animation,\n  delay,\n  index,\n  inView,\n  value,\n}: {\n  animation: TextAnimateAnimation;\n  delay: number;\n  index: number;\n  inView: boolean;\n  value: string;\n}) {\n  const blur = animation === \"blur\";\n  return (\n    <span\n      className={cn(\n        \"inline-block whitespace-pre\",\n        blur\n          ? cn(\n              \"transition-[filter,opacity] duration-500\",\n              inView ? \"opacity-100 [filter:blur(0)]\" : animationClasses.blur,\n            )\n          : inView && cn(\"fill-mode-both\", animationClasses[animation]),\n      )}\n      style={{ animationDelay: `${index * delay}ms` }}\n    >\n      {value}\n    </span>\n  );\n}\n\n/**\n * Reveals text segment-by-segment with a staggered entrance.\n *\n * Respects `prefers-reduced-motion`: the text shows at once.\n *\n * @example\n * ```tsx\n * <TextAnimate animation=\"slide-up\">Welcome aboard</TextAnimate>\n * ```\n */\nexport const TextAnimate = ({\n  animation = \"fade\",\n  by = \"word\",\n  children,\n  className,\n  delay = 60,\n  ref,\n  startOnView = true,\n  ...props\n}: TextAnimateProps & { ref?: React.Ref<HTMLDivElement> }) => {\n  const reduced = usePrefersReducedMotion();\n  const [nodeRef, inView] = useInView(startOnView);\n  const segments = splitText(children, by);\n  const visible = reduced || inView;\n\n  return (\n    <div\n      className={cn(className)}\n      ref={(node) => {\n        nodeRef.current = node;\n        assignRef(ref, node);\n      }}\n      {...props}\n    >\n      <span aria-hidden=\"true\">\n        {segments.map((value, index) => (\n          <Segment\n            animation={reduced ? \"fade\" : animation}\n            delay={reduced ? 0 : delay}\n            index={index}\n            inView={visible}\n            key={`${value}-${index}`}\n            value={value}\n          />\n        ))}\n      </span>\n      <span className=\"sr-only\">{children}</span>\n    </div>\n  );\n};\nTextAnimate.displayName = \"TextAnimate\";\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.2.1",
  "stability": "stable"
}
