{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"line-reveal","type":"registry:component","title":"Line Reveal","description":"Headings and paragraphs that rise line by line out of a mask.","author":"DiMaac (HankTheTank) <https://dimaac.com>","dependencies":["gsap","@gsap/react"],"files":[{"path":"registry/dimaac/line-reveal/line-reveal.tsx","type":"registry:component","target":"components/dimaac/line-reveal.tsx","content":"\"use client\";\n\nimport { createElement, useRef } from \"react\";\nimport { gsap } from \"gsap\";\nimport { ScrollTrigger } from \"gsap/ScrollTrigger\";\nimport { SplitText } from \"gsap/SplitText\";\nimport { useGSAP } from \"@gsap/react\";\n\n// A heading or paragraph that rises line by line out of a mask when it scrolls into view.\n// Optionally the words start faint and fill in as you scroll, like reading along with your finger.\n// The text stays real text: selectable, and read out as one sentence by screen readers.\n\nif (typeof window !== \"undefined\") gsap.registerPlugin(useGSAP, ScrollTrigger, SplitText);\n\nexport type LineRevealProps = {\n  /** The text to show. Plain text only (the lines are cut from it). */\n  children: string;\n  /** The tag to render: a page title, a section title or a paragraph. */\n  as?: \"h1\" | \"h2\" | \"p\";\n  /** Delay between one line starting and the next, in seconds. */\n  stagger?: number;\n  /** How long each line takes to rise, in seconds. */\n  duration?: number;\n  /** Fill the words in one by one as you scroll through the text. */\n  highlight?: boolean;\n  /** The colour a word turns when it is filled in. Empty keeps the text colour. */\n  highlightColor?: string;\n  /** When the reveal starts: \"<element edge> <viewport edge>\", e.g. \"top 85%\". */\n  start?: string;\n  /** The scrolling element. Leave it out to use the page; give it a selector or an element for an inner scroller. */\n  scroller?: string | HTMLElement;\n  /** Extra classes: put the font size, weight and colour here. */\n  className?: string;\n};\n\nexport function LineReveal({\n  children,\n  as = \"p\",\n  stagger = 0.08,\n  duration = 1,\n  highlight = false,\n  highlightColor = \"#ff5a36\",\n  start = \"top 85%\",\n  scroller,\n  className = \"\",\n}: LineRevealProps) {\n  const textRef = useRef<HTMLElement>(null);\n\n  useGSAP(\n    () => {\n      const element = textRef.current;\n      if (!element) return;\n\n      // The text is hidden until it is split, so you never see it jump. Show it again either way.\n      element.style.visibility = \"visible\";\n\n      // Reduced motion: leave the text exactly as written, fully readable\n      if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n\n      let fill: gsap.core.Tween | undefined;\n\n      const split = SplitText.create(element, {\n        type: highlight ? \"lines,words\" : \"lines\",\n        mask: \"lines\",\n        linesClass: \"line-reveal-line\",\n        // Re-cut the lines when the width changes or the web font arrives\n        autoSplit: true,\n        onSplit(self) {\n          // The mask clips to the line box, which cuts the tails of g, y and p. Give it a little room.\n          self.masks.forEach((mask) => {\n            const box = mask as HTMLElement;\n            box.style.paddingBottom = \"0.14em\";\n            box.style.marginBottom = \"-0.14em\";\n          });\n\n          // This runs again after every re-split, so drop the old fill first\n          fill?.scrollTrigger?.kill();\n          fill?.kill();\n\n          if (highlight) {\n            const color = highlightColor || getComputedStyle(element).color;\n            // Words start faint and fill in as the scrollbar moves (scrub), one after another\n            fill = gsap.fromTo(\n              self.words,\n              { opacity: 0.18 },\n              {\n                opacity: 1,\n                color,\n                ease: \"none\",\n                stagger: 0.12,\n                duration: 0.4,\n                scrollTrigger: { trigger: element, scroller, start, end: \"bottom 45%\", scrub: 0.4 },\n              },\n            );\n          }\n\n          // Returning the tween lets SplitText keep it in step when it re-splits\n          return gsap.from(self.lines, {\n            yPercent: 115,\n            rotate: 3,\n            transformOrigin: \"0% 100%\",\n            duration,\n            ease: \"power4.out\",\n            stagger,\n            scrollTrigger: { trigger: element, scroller, start, toggleActions: \"play none none none\" },\n          });\n        },\n      });\n\n      return () => {\n        fill?.scrollTrigger?.kill();\n        fill?.kill();\n        split.revert();\n      };\n    },\n    { scope: textRef, dependencies: [children, as, stagger, duration, highlight, highlightColor, start, scroller], revertOnUpdate: true },\n  );\n\n  // The key makes React build a fresh element when the text or tag changes, because SplitText\n  // rewrites the inside of the old one.\n  return createElement(as, { ref: textRef, key: `${as}:${children}`, className, style: { visibility: \"hidden\" } }, children);\n}\n"}],"categories":["text","gsap","splittext","scrolltrigger","editorial"],"docs":"import { LineReveal } from \"@/components/dimaac/line-reveal\". Give its parent a size: it fills it. Docs, playground and props: https://ui.dimaac.com/components/line-reveal","meta":{"tier":"free","preview":"https://ui.dimaac.com/components/line-reveal","credits":[]}}