{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cascade-text",
  "title": "Cascade Text",
  "description": "A smooth line-by-line text reveal animation.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/react/atlasui/cascade-text.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef } from \"react\";\n\nimport {\n  animate,\n  cubicBezier,\n  inView,\n  stagger,\n  useReducedMotion,\n} from \"motion/react\";\n\ninterface CascadeTextProps {\n  children: React.ReactNode;\n  animateOnScroll?: boolean;\n  delay?: number;\n}\n\ntype PreparedElement = {\n  element: HTMLElement;\n  originalHTML: string;\n  originalTextIndent: string;\n  lines: HTMLElement[];\n};\n\nconst getTargetElements = (container: HTMLElement): HTMLElement[] => {\n  if (container.hasAttribute(\"data-cascade-text\")) {\n    return Array.from(container.children) as HTMLElement[];\n  }\n\n  return [container];\n};\n\nconst splitIntoLines = (\n  element: HTMLElement,\n  reduceMotion: boolean\n): HTMLElement[] => {\n  const text = (element.textContent ?? \"\").trim();\n\n  if (!text) {\n    element.textContent = \"\";\n    return [];\n  }\n\n  const words = text.split(/\\s+/).filter(Boolean);\n  const fragment = document.createDocumentFragment();\n  const wordSpans: HTMLSpanElement[] = [];\n\n  for (let index = 0; index < words.length; index += 1) {\n    const span = document.createElement(\"span\");\n    span.textContent = words[index];\n    span.style.display = \"inline-block\";\n\n    wordSpans.push(span);\n    fragment.appendChild(span);\n\n    if (index < words.length - 1) {\n      fragment.appendChild(document.createTextNode(\" \"));\n    }\n  }\n\n  element.textContent = \"\";\n  element.appendChild(fragment);\n\n  const linesText: string[] = [];\n  let currentTop: number | null = null;\n  let currentWords: string[] = [];\n\n  wordSpans.forEach((span) => {\n    const top = span.offsetTop;\n    const value = span.textContent ?? \"\";\n\n    if (currentTop === null) {\n      currentTop = top;\n      currentWords.push(value);\n      return;\n    }\n\n    if (top !== currentTop) {\n      linesText.push(currentWords.join(\" \"));\n      currentWords = [value];\n      currentTop = top;\n      return;\n    }\n\n    currentWords.push(value);\n  });\n\n  if (currentWords.length > 0) {\n    linesText.push(currentWords.join(\" \"));\n  }\n\n  element.textContent = \"\";\n\n  const lines: HTMLElement[] = [];\n\n  linesText.forEach((lineText) => {\n    const mask = document.createElement(\"span\");\n    mask.style.display = \"block\";\n    mask.style.overflow = \"hidden\";\n\n    const line = document.createElement(\"span\");\n    line.textContent = lineText;\n    line.style.display = \"block\";\n    line.style.willChange = reduceMotion ? \"opacity\" : \"transform\";\n\n    if (reduceMotion) {\n      line.style.opacity = \"0\";\n    } else {\n      line.style.transform = \"translateY(100%)\";\n    }\n\n    mask.appendChild(line);\n    element.appendChild(mask);\n    lines.push(line);\n  });\n\n  return lines;\n};\n\nconst prepareElement = (\n  element: HTMLElement,\n  reduceMotion: boolean\n): PreparedElement => {\n  const originalHTML = element.innerHTML;\n  const originalTextIndent = element.style.textIndent;\n  const lines = splitIntoLines(element, reduceMotion);\n  const computedIndent = window.getComputedStyle(element).textIndent;\n\n  if (computedIndent !== \"0px\" && lines.length > 0) {\n    lines[0].style.paddingLeft = computedIndent;\n    element.style.textIndent = \"0\";\n  }\n\n  return {\n    element,\n    originalHTML,\n    originalTextIndent,\n    lines,\n  };\n};\n\nconst animateLines = (\n  lines: HTMLElement[],\n  delay: number,\n  reduceMotion: boolean\n) => {\n  if (lines.length === 0) {\n    return;\n  }\n\n  return reduceMotion\n    ? animate(\n        lines,\n        { opacity: [0, 1] },\n        {\n          duration: 0.28,\n          delay: stagger(0.06, { startDelay: delay }),\n          ease: \"easeOut\",\n        }\n      )\n    : animate(\n        lines,\n        { y: [\"100%\", \"0%\"] },\n        {\n          duration: 1,\n          delay: stagger(0.1, { startDelay: delay }),\n          ease: cubicBezier(0.165, 0.84, 0.44, 1),\n        }\n      );\n};\n\nconst restoreElements = (preparedElements: PreparedElement[]) => {\n  preparedElements.forEach(({ element, originalHTML, originalTextIndent }) => {\n    element.innerHTML = originalHTML;\n    element.style.textIndent = originalTextIndent;\n  });\n};\n\nexport const CascadeText = ({\n  children,\n  animateOnScroll = true,\n  delay = 0,\n}: CascadeTextProps) => {\n  const containerRef = useRef<HTMLElement | null>(null);\n  const reduceMotion = useReducedMotion() ?? false;\n\n  useEffect(() => {\n    const container = containerRef.current;\n\n    if (!container) {\n      return;\n    }\n\n    const preparedElements = getTargetElements(container).map((element) =>\n      prepareElement(element, reduceMotion)\n    );\n    const lines = preparedElements.flatMap((item) => item.lines);\n\n    let stopInView: (() => void) | undefined;\n    const controls = {\n      current: undefined as ReturnType<typeof animate> | undefined,\n    };\n\n    const runAnimation = () => {\n      controls.current = animateLines(lines, delay, reduceMotion);\n    };\n\n    if (animateOnScroll) {\n      stopInView = inView(\n        container,\n        () => {\n          runAnimation();\n          stopInView?.();\n        },\n        { margin: \"0px 0px -25% 0px\" }\n      );\n    } else {\n      runAnimation();\n    }\n\n    return () => {\n      stopInView?.();\n      controls.current?.stop();\n      restoreElements(preparedElements);\n    };\n  }, [animateOnScroll, delay, reduceMotion]);\n\n  /* eslint-disable react-hooks/refs */\n  if (React.Children.count(children) === 1) {\n    return React.cloneElement(\n      children as React.ReactElement<{ ref?: React.Ref<HTMLElement> }>,\n      {\n        ref: containerRef,\n      }\n    );\n  }\n  /* eslint-enable react-hooks/refs */\n\n  return (\n    <div\n      ref={containerRef as React.RefObject<HTMLDivElement>}\n      data-cascade-text=\"true\"\n    >\n      {children}\n    </div>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}