{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cascade-text-html",
  "title": "Cascade Text",
  "description": "A smooth line-by-line text reveal animation built with HTML, CSS, JavaScript and Motion.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/html/atlasui/cascade-text/index.html",
      "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Cascade Text - Atlas UI</title>\n    <link rel=\"stylesheet\" href=\"style.css\" />\n  </head>\n  <body>\n    <main>\n      <h1 data-cascade-text>Cascade Text: Designed to Move, Not Distract.</h1>\n\n      <p data-cascade-text data-delay=\"0.2\">\n        You could reveal everything instantly. You could also microwave a steak.\n        We chose patience. Because good interfaces, like good food, deserve a\n        little timing and a little respect.\n      </p>\n\n      <p data-cascade-text data-delay=\"0.4\">\n        Cascade Text brings in each line with just enough drama to feel\n        intentional, and just enough restraint to keep it classy. No\n        attention-seeking theatrics. Just a steady, confident rise that feels\n        composed from start to finish.\n      </p>\n\n      <p data-cascade-text data-delay=\"0.6\">\n        It's smooth. It's subtle. It doesn't beg for attention. It just quietly\n        says, \"Yeah… this UI knows what it's doing.\" Because when motion is done\n        right, users don't notice the animation. They notice how effortless\n        everything feels. And that quiet effortlessness? That's the whole point.\n      </p>\n\n      <p data-cascade-text data-delay=\"0.8\">\n        I'm quite surprised that you are still reading this. But I'm glad you\n        made it this far. So go ahead and explore the rest of the Atlas UI\n        components.\n      </p>\n    </main>\n  </body>\n\n  <script type=\"module\" src=\"script.js\"></script>\n</html>\n",
      "type": "registry:file",
      "target": "~/cascade-text/index.html"
    },
    {
      "path": "registry/html/atlasui/cascade-text/style.css",
      "content": ":root {\n  --background: light-dark(\n    oklch(100% 0.00011 271.152),\n    oklch(18.22% 0.00002 271.152)\n  );\n  --foreground: light-dark(\n    oklch(18.22% 0.00002 271.152),\n    oklch(98.511% 0.00011 271.152)\n  );\n}\n\nhtml {\n  color-scheme: light dark;\n}\n\n* {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n}\n\nbody {\n  min-height: 100svh;\n  padding: 4rem 1.25rem;\n  background-color: var(--background);\n  color: var(--foreground);\n  font-family: \"Inter\", sans-serif;\n  max-width: 800px;\n  margin: 0 auto;\n}\n\np {\n  font-size: clamp(1rem, 1.15vw + 0.8rem, 1.25rem);\n  line-height: 1.45;\n  margin-block: 1rem;\n}\n\n/* These are the important classes, you can tweak above styles as per you need */\n\n[data-mask] {\n  display: block;\n  overflow: hidden;\n}\n\n[data-line] {\n  display: block;\n  will-change: transform;\n}\n",
      "type": "registry:file",
      "target": "~/cascade-text/style.css"
    },
    {
      "path": "registry/html/atlasui/cascade-text/script.js",
      "content": "import { animate, cubicBezier, inView, stagger } from \"motion\";\n\nconst REVEAL_EASE = cubicBezier(0.165, 0.84, 0.44, 1);\nconst SCROLL_MARGIN = \"0px 0px -25% 0px\";\nconst reduceMotionQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\nconst splitIntoLines = (element, reduceMotion) => {\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 = [];\n\n  for (let index = 0; index < words.length; index += 1) {\n    const wordSpan = document.createElement(\"span\");\n    wordSpan.textContent = words[index];\n    wordSpan.style.display = \"inline-block\";\n\n    wordSpans.push(wordSpan);\n    fragment.appendChild(wordSpan);\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 = [];\n  let currentTop = null;\n  let currentWords = [];\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 = [];\n\n  linesText.forEach((lineText) => {\n    const mask = document.createElement(\"span\");\n    mask.setAttribute(\"data-mask\", \"\");\n\n    const line = document.createElement(\"span\");\n    line.setAttribute(\"data-line\", \"\");\n    line.textContent = lineText;\n\n    if (reduceMotion) {\n      line.style.opacity = \"0\";\n      line.style.willChange = \"opacity\";\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 = (element, reduceMotion) => {\n  const lines = splitIntoLines(element, reduceMotion);\n  const textIndent = window.getComputedStyle(element).textIndent;\n\n  if (textIndent !== \"0px\" && lines.length > 0) {\n    lines[0].style.paddingLeft = textIndent;\n    element.style.textIndent = \"0\";\n  }\n\n  return lines;\n};\n\nconst animateLines = (lines, startDelay, reduceMotion) => {\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 }),\n          ease: \"easeOut\",\n        },\n      )\n    : animate(\n        lines,\n        { y: [\"100%\", \"0%\"] },\n        {\n          duration: 1,\n          delay: stagger(0.1, { startDelay }),\n          ease: REVEAL_EASE,\n        },\n      );\n};\n\ndocument.querySelectorAll(\"[data-cascade-text]\").forEach((element) => {\n  const delay = Number.parseFloat(element.dataset.delay ?? \"0\") || 0;\n  const animateOnScroll = element.dataset.animateOnScroll !== \"false\";\n  const reduceMotion = reduceMotionQuery.matches;\n  const lines = prepareElement(element, reduceMotion);\n\n  const run = () => animateLines(lines, delay, reduceMotion);\n\n  if (!animateOnScroll) {\n    run();\n    return;\n  }\n\n  let stopInView;\n  stopInView = inView(\n    element,\n    () => {\n      run();\n      stopInView?.();\n    },\n    { margin: SCROLL_MARGIN },\n  );\n});\n",
      "type": "registry:file",
      "target": "~/cascade-text/script.js"
    }
  ],
  "meta": {
    "framework": "html"
  },
  "type": "registry:file"
}