{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "top-loader",
  "type": "registry:ui",
  "title": "Top Loader",
  "description": "A thin page bar that trickles, finishes fast and fades; skips fast loads.",
  "categories": [
    "async"
  ],
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json"
  ],
  "files": [
    {
      "path": "components/ui/top-loader.tsx",
      "content": "\"use client\";\nimport { Progress } from \"@base-ui/react/progress\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\ntype Phase = \"idle\" | \"waiting\" | \"trickling\" | \"finishing\" | \"fading\";\n\n/**\n * start() / done() / fail() with a counter, so overlapping requests keep one bar\n * running until the last of them settles. track(promise) does both for you.\n */\nexport function useTopLoader() {\n  const [count, setCount] = useState(0);\n  const [error, setError] = useState(false);\n  const start = useCallback(() => {\n    setError(false);\n    setCount((c) => c + 1);\n  }, []);\n  const done = useCallback(() => setCount((c) => Math.max(0, c - 1)), []);\n  const fail = useCallback(() => {\n    setError(true);\n    setCount((c) => Math.max(0, c - 1));\n  }, []);\n  const track = useCallback(\n    <T,>(promise: Promise<T>) => {\n      start();\n      promise.then(done, fail);\n      return promise;\n    },\n    [start, done, fail],\n  );\n  const loading = count > 0;\n  return useMemo(() => ({ loading, error, start, done, fail, track }), [loading, error, start, done, fail, track]);\n}\n\nexport type TopLoaderProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** True while the page (or anything) is loading. Flip it back to false to finish. */\n  loading: boolean;\n  /** Finish in the danger color, for a navigation that failed. */\n  error?: boolean;\n  /** Milliseconds before the bar appears. Loads that finish sooner never show it. */\n  delay?: number;\n  /** fixed pins it to the top of the viewport; absolute to the top of the nearest positioned parent. */\n  position?: \"fixed\" | \"absolute\";\n  /** Accessible name while visible. */\n  label?: string;\n};\n\n// Where the trickle heads: fast at first, slowing as it nears 94%, never arriving\n// on its own. Each step covers a share of the remaining distance.\nconst nextTrickle = (p: number, step: number) => Math.min(0.94, p + (0.94 - p) * (0.1 + (step % 3) * 0.04));\n\nexport function TopLoader({ loading, error = false, delay = 120, position = \"fixed\", label = \"Loading page\", className, style, ...rest }: TopLoaderProps) {\n  const [phase, setPhase] = useState<Phase>(loading ? \"waiting\" : \"idle\");\n  const [p, setP] = useState(0);\n  const [prev, setPrev] = useState(loading);\n  const steps = useRef(0);\n\n  // Follow the prop on the render it changes.\n  if (loading !== prev) {\n    setPrev(loading);\n    if (loading) {\n      if (phase === \"idle\") setPhase(\"waiting\");\n      else if (phase === \"finishing\" || phase === \"fading\") {\n        // A new load while the last one is leaving: start again from the left, unanimated.\n        setP(0);\n        setPhase(\"waiting\");\n      }\n    } else if (phase === \"waiting\") setPhase(\"idle\");\n    else if (phase === \"trickling\") {\n      setP(1);\n      setPhase(\"finishing\");\n    }\n  }\n  useEffect(() => {\n    if (phase === \"waiting\") {\n      const id = window.setTimeout(() => {\n        steps.current = 0;\n        setP(0.08);\n        setPhase(\"trickling\");\n      }, delay);\n      return () => window.clearTimeout(id);\n    }\n    if (phase === \"trickling\") {\n      const id = window.setInterval(() => {\n        // Don't creep forward in a hidden tab; nobody is watching and it would arrive stale.\n        if (document.hidden) return;\n        steps.current += 1;\n        setP((x) => nextTrickle(x, steps.current));\n      }, 480);\n      return () => window.clearInterval(id);\n    }\n    if (phase === \"finishing\") {\n      const id = window.setTimeout(() => setPhase(\"fading\"), error ? 520 : 240);\n      return () => window.clearTimeout(id);\n    }\n    if (phase === \"fading\") {\n      const id = window.setTimeout(() => {\n        setP(0);\n        setPhase(\"idle\");\n      }, 320);\n      return () => window.clearTimeout(id);\n    }\n  }, [phase, delay, error]);\n\n  const visible = phase === \"trickling\" || phase === \"finishing\";\n  // Reset to 0 happens while invisible, so it must not animate backwards.\n  const moving = phase === \"trickling\" ? \"duration-[600ms]\" : phase === \"finishing\" ? \"duration-200\" : \"duration-0\";\n\n  return (\n    <Progress.Root\n      value={visible ? Math.round(p * 100) : null}\n      aria-label={label}\n      aria-hidden={!visible || undefined}\n      data-state={phase}\n      data-error={error || undefined}\n      className={cn(\n        \"pointer-events-none inset-x-0 top-0 z-(--z-toast) h-0.5 overflow-hidden transition-opacity ease-out\",\n        position === \"fixed\" ? \"fixed\" : \"absolute\",\n        visible ? \"opacity-100 duration-150\" : \"opacity-0 duration-300\",\n        className,\n      )}\n      style={style}\n      {...rest}\n    >\n      <Progress.Indicator\n        className={cn(\n          \"absolute inset-y-0 left-0 transition-[transform,background-color] ease-out-quart motion-reduce:duration-0\",\n          moving,\n          error && phase !== \"trickling\" ? \"bg-danger\" : \"bg-fg\",\n        )}\n        // Full width, slid left: composited, and the head stays crisp.\n        style={{ width: \"100%\", transform: `translateX(${(p - 1) * 100}%)` }}\n      />\n    </Progress.Root>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/top-loader"
}
