{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-reveal",
  "type": "registry:ui",
  "title": "Scroll Reveal",
  "description": "Reveals children once as they scroll in, with a capped stagger that never replays.",
  "categories": [
    "scroll"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/motion.json"
  ],
  "files": [
    {
      "path": "components/ui/scroll-reveal.tsx",
      "content": "\"use client\";\nimport { useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ease } from \"@/lib/motion\";\n\nexport type ScrollRevealProps = React.HTMLAttributes<HTMLElement> & {\n  /** The element to render. Children are revealed, not the element itself. */\n  as?: \"div\" | \"section\" | \"ul\" | \"ol\" | \"article\";\n  /** Reveal each child as it enters, instead of the whole group at once. Use it for long lists and grids. */\n  each?: boolean;\n  /** Seconds between children arriving together. */\n  stagger?: number;\n  /** After this many children, the rest arrive with the last one's delay, so a long list never trails. */\n  staggerCap?: number;\n  /** Seconds for each child's entrance. */\n  duration?: number;\n  /** Seconds before the first child. */\n  delay?: number;\n  /** Rise in px. */\n  distance?: number;\n  /** Starting blur in px. 0 turns it off for large or image-heavy children. */\n  blur?: number;\n  /** The scroll container, when it isn't the page. */\n  root?: React.RefObject<Element | null>;\n  /** Fraction of each child that must be visible to reveal it (with each). A group reveals as its top edge enters. */\n  threshold?: number;\n  rootMargin?: string;\n  ref?: React.Ref<HTMLElement>;\n};\n\n/**\n * Reveals its children once, as they scroll into view: a fade, an 8px rise and\n * a blur that clears. It never plays again. With reduced motion, or without\n * JavaScript, the children are simply there.\n */\nexport function ScrollReveal({\n  as: Tag = \"div\",\n  each = false,\n  stagger = 0.05,\n  staggerCap = 6,\n  duration = 0.6,\n  delay = 0,\n  distance = 8,\n  blur = 4,\n  root,\n  threshold = 0.2,\n  rootMargin = \"0px 0px -8% 0px\",\n  className,\n  children,\n  ref,\n  ...rest\n}: ScrollRevealProps) {\n  const reduce = useReducedMotion();\n  const self = useRef<HTMLElement>(null);\n  const opts = useRef({ each, stagger, staggerCap, duration, delay, distance, blur });\n  useEffect(() => {\n    opts.current = { each, stagger, staggerCap, duration, delay, distance, blur };\n  });\n\n  useEffect(() => {\n    const el = self.current;\n    if (!el) return;\n    const pending = () => Array.from(el.children).filter((c): c is HTMLElement => c instanceof HTMLElement && !c.hasAttribute(\"data-revealed\"));\n\n    // Nothing to animate: mark everything shown so the hiding rule lets go.\n    if (reduce) {\n      pending().forEach((c) => c.setAttribute(\"data-revealed\", \"\"));\n      return;\n    }\n\n    const play = (targets: HTMLElement[]) => {\n      const o = opts.current;\n      targets.forEach((child, i) => {\n        child.setAttribute(\"data-revealed\", \"\");\n        // Web Animations with fill \"backwards\": hidden through its delay, and\n        // nothing left behind afterwards to fight the child's own styles.\n        child.animate(\n          [\n            { opacity: 0, transform: `translateY(${o.distance}px)`, filter: o.blur ? `blur(${o.blur}px)` : \"none\" },\n            { opacity: 1, transform: \"none\", filter: o.blur ? \"blur(0px)\" : \"none\" },\n          ],\n          { duration: o.duration * 1000, delay: (o.delay + Math.min(i, o.staggerCap) * o.stagger) * 1000, easing: `cubic-bezier(${ease.out.join(\",\")})`, fill: \"backwards\" },\n        );\n      });\n    };\n\n    const io = new IntersectionObserver(\n      (entries) => {\n        const arriving: HTMLElement[] = [];\n        for (const entry of entries) {\n          const target = entry.target as HTMLElement;\n          const above = entry.boundingClientRect.bottom < (entry.rootBounds?.top ?? 0);\n          if (!entry.isIntersecting && !above) continue;\n          io.unobserve(target);\n          const kids = target === el ? pending() : [target];\n          // Jumped past it (an anchor link, End, a restored scroll): show it without a show.\n          if (above) kids.forEach((k) => k.setAttribute(\"data-revealed\", \"\"));\n          else arriving.push(...kids);\n        }\n        // Children arriving in the same frame stagger in document order.\n        arriving.sort((a, b) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1));\n        if (arriving.length) play(arriving);\n      },\n      { root: root?.current ?? null, threshold: each ? threshold : 0, rootMargin },\n    );\n\n    const watch = () => {\n      if (opts.current.each) pending().forEach((c) => io.observe(c));\n      else io.observe(el);\n    };\n    watch();\n\n    // Children added later (the next page of a feed) reveal as they enter, one by one.\n    const mo = new MutationObserver(() => pending().forEach((c) => io.observe(c)));\n    mo.observe(el, { childList: true });\n\n    return () => {\n      io.disconnect();\n      mo.disconnect();\n    };\n    // Observers are built once; the options above are read live.\n  }, [reduce, root, threshold, rootMargin]);\n\n  return (\n    <>\n      <Tag\n        ref={(node: HTMLElement | null) => {\n          self.current = node;\n          if (typeof ref === \"function\") return ref(node);\n          if (ref) (ref as React.RefObject<HTMLElement | null>).current = node;\n        }}\n        data-scroll-reveal=\"\"\n        className={cn(\n          // Hidden until revealed, but only when motion is welcome; reduced motion never hides anything.\n          \"motion-safe:[&>*:not([data-revealed])]:opacity-0 print:[&>*]:opacity-100!\",\n          className,\n        )}\n        {...rest}\n      >\n        {children}\n      </Tag>\n      {/* Without JavaScript nothing would ever reveal, so nothing hides. */}\n      <noscript>\n        <style>{\"[data-scroll-reveal]>*{opacity:1!important}\"}</style>\n      </noscript>\n    </>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/scroll-reveal"
}
