{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stagger-in",
  "type": "registry:ui",
  "title": "Stagger In",
  "description": "Rows arrive in one capped wave the first time, never on filters.",
  "categories": [
    "motion"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json"
  ],
  "files": [
    {
      "path": "components/ui/stagger-in.tsx",
      "content": "\"use client\";\nimport { useEffect, useRef, useState } from \"react\";\n\n/**\n * The entrance is plain CSS so it runs on the server-rendered HTML at first\n * paint, before hydration, with nothing hidden if scripts never load. Each\n * direct child reads its place from :nth-child, so there is no index to pass\n * and no wrapper per item. Once the first wave has played the rule is removed:\n * rows added, filtered or re-sorted later simply appear.\n */\n// Positions past the cap all share its delay, so rules are only needed up to the largest cap.\nconst MAX_CAP = 16;\nconst CSS = `\n@keyframes stealth-stagger-in { from { opacity: 0; transform: translateY(var(--stagger-distance)); } }\n@keyframes stealth-stagger-fade { from { opacity: 0; } }\n[data-stagger-in=\"play\"] > * {\n  animation: stealth-stagger-in var(--stagger-duration) var(--ease-out-expo) backwards;\n  animation-delay: calc(var(--stagger-delay) + min(var(--stagger-i, 0), var(--stagger-tail)) * var(--stagger-step));\n}\n${Array.from({ length: MAX_CAP - 1 }, (_, i) => `[data-stagger-in=\"play\"] > :nth-child(${i + 2}) { --stagger-i: ${i + 1}; }`).join(\"\\n\")}\n[data-stagger-in=\"play\"] > :nth-child(n + ${MAX_CAP + 1}) { --stagger-i: ${MAX_CAP - 1}; }\n@media (prefers-reduced-motion: reduce) {\n  [data-stagger-in=\"play\"] > * { animation-name: stealth-stagger-fade; animation-duration: 160ms; animation-delay: 0ms; }\n}\n`;\n\nexport type StaggerInProps = React.ComponentProps<\"div\"> & {\n  /** The list element to render. Every direct child is one staggered item. */\n  as?: \"div\" | \"ul\" | \"ol\" | \"section\" | \"tbody\";\n  /** Milliseconds between one item and the next. */\n  step?: number;\n  /** Items after this many arrive together with the last staggered one, so a long list never makes the reader wait for its tail. 1–16. */\n  cap?: number;\n  /** Milliseconds before the first item starts. */\n  delay?: number;\n  /** Milliseconds each item takes to arrive. */\n  duration?: number;\n  /** Pixels each item rises from. */\n  distance?: number;\n  /** Skip the entrance entirely, e.g. when the list is restored from a cache the user has already seen. */\n  disabled?: boolean;\n};\n\n/** The delay for item `index` under the same capped stagger, in seconds, for Motion-driven items. */\nexport function staggerDelay(index: number, { step = 22, cap = 8, delay = 0 }: { step?: number; cap?: number; delay?: number } = {}) {\n  return (delay + Math.min(index, cap - 1) * step) / 1000;\n}\n\ntype Phase = \"play\" | \"done\";\n\n/**\n * Plays a capped stagger the first time items appear in it: on mount if it\n * mounts with data, or when the first rows arrive after a loading state.\n * Remount it (change its key) to play it again.\n */\nexport function StaggerIn({\n  as: Tag = \"div\",\n  step = 22,\n  cap = 8,\n  delay = 0,\n  duration = 360,\n  distance = 6,\n  disabled = false,\n  className,\n  style,\n  children,\n  onAnimationStart,\n  ...rest\n}: StaggerInProps) {\n  const tail = Math.min(Math.max(Math.round(cap), 1), MAX_CAP) - 1;\n  const [phase, setPhase] = useState<Phase>(disabled ? \"done\" : \"play\");\n  const timer = useRef<number>(undefined);\n  useEffect(() => () => window.clearTimeout(timer.current), []);\n\n  // The first item to start opens a window just long enough for the whole wave;\n  // anything that mounts inside it joins the wave, anything after it doesn't animate.\n  const handleStart = (e: React.AnimationEvent<HTMLDivElement>) => {\n    onAnimationStart?.(e);\n    if (e.animationName !== \"stealth-stagger-in\" && e.animationName !== \"stealth-stagger-fade\") return;\n    // Only this list's own items; a nested StaggerIn runs its own wave.\n    if ((e.target as Element).parentElement !== e.currentTarget) return;\n    if (timer.current !== undefined) return;\n    const wave = delay + tail * step + duration + 60;\n    timer.current = window.setTimeout(() => setPhase(\"done\"), wave);\n  };\n\n  const Comp = Tag as \"div\";\n  return (\n    <Comp\n      data-stagger-in={disabled ? \"done\" : phase}\n      className={className}\n      style={\n        {\n          \"--stagger-step\": `${step}ms`,\n          \"--stagger-delay\": `${delay}ms`,\n          \"--stagger-duration\": `${duration}ms`,\n          \"--stagger-distance\": `${distance}px`,\n          \"--stagger-tail\": tail,\n          ...style,\n        } as React.CSSProperties\n      }\n      onAnimationStart={handleStart}\n      {...rest}\n    >\n      <style href=\"stealth-stagger-in\" precedence=\"default\">\n        {CSS}\n      </style>\n      {children}\n    </Comp>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/stagger-in"
}
