{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "preloader",
  "title": "Preloader",
  "description": "A full-screen cover that counts to 100 on first load, waits for your content to be ready, then fades out.",
  "registryDependencies": [
    "@corsair-ui/utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/preloader.tsx",
      "content": "\"use client\";\n\nimport {\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type ReactNode,\n  type Ref,\n} from \"react\";\n\nimport { cn } from \"@/registry/default/lib/utils\";\n\nfunction mergeRefs<T>(...refs: (Ref<T> | undefined)[]) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    }\n  };\n}\n\nfunction prefersReducedMotion() {\n  return window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches === true;\n}\n\nconst easeOut = (t: number) => 1 - (1 - t) ** 3;\n\n// While `ready` is false the count creeps toward this and waits.\nconst WAITING_CEILING = 90;\n\ntype PreloaderPhase = \"loading\" | \"done\" | \"gone\";\n\ninterface PreloaderProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n  /** The shortest time it stays on screen, in ms. */\n  duration?: number;\n  /** Whether whatever it waits for has arrived. While false, the count stops short of 90. */\n  ready?: boolean;\n  /** Called once, when the overlay has faded out and unmounted. */\n  onComplete?: () => void;\n  /** Accessible name of the progress bar. */\n  label?: string;\n  /** How long the fade out takes, in ms. */\n  exitDuration?: number;\n  /** What it shows: anything, or a function of the progress from 0 to 100. */\n  children?: ReactNode | ((progress: number) => ReactNode);\n}\n\n/**\n * A full-screen cover for the first load: it counts from 0 to 100 over at\n * least `duration`, waits at 90 while `ready` is false, then fades out and\n * unmounts itself. The count is written to `--preloader-progress` (0 to 1)\n * once per frame for bars like `PreloaderBar`, and handed to `children` as\n * a whole number when it is a function.\n *\n * It is a `progressbar` with a name and a value. Keep the page behind it\n * `inert` until `onComplete`, so keyboard and screen reader users cannot\n * wander into content they cannot see. With `prefers-reduced-motion` it\n * skips the count and the fade and leaves as soon as `ready` is true.\n * Without JavaScript it never covers the page.\n *\n * @example\n * const [loaded, setLoaded] = useState(false);\n * <Preloader onComplete={() => setLoaded(true)}>\n *   {(progress) => (\n *     <div className=\"grid gap-3 text-center\">\n *       <span className=\"font-mono text-sm tabular-nums\">{progress}%</span>\n *       <PreloaderBar />\n *     </div>\n *   )}\n * </Preloader>\n * <main inert={!loaded}>…</main>\n */\nfunction Preloader({\n  duration = 1500,\n  ready = true,\n  onComplete,\n  label = \"Loading\",\n  exitDuration = 500,\n  children,\n  className,\n  style,\n  ref,\n  ...props\n}: PreloaderProps) {\n  const root = useRef<HTMLDivElement>(null);\n  const mergedRef = useMemo(() => mergeRefs(ref, root), [ref]);\n  const [progress, setProgress] = useState(0);\n  const [phase, setPhase] = useState<PreloaderPhase>(\"loading\");\n  const value = useRef(0);\n  const started = useRef<number | null>(null);\n\n  const onCompleteRef = useRef(onComplete);\n  useEffect(() => {\n    onCompleteRef.current = onComplete;\n  });\n\n  // Count once per frame, straight into a CSS variable; React only hears whole numbers.\n  useEffect(() => {\n    if (phase !== \"loading\") return;\n    const node = root.current;\n    let frame = 0;\n    const write = (next: number) => {\n      value.current = next;\n      node?.style.setProperty(\"--preloader-progress\", String(next / 100));\n      setProgress(Math.round(next));\n    };\n    if (prefersReducedMotion()) {\n      if (!ready) return;\n      frame = requestAnimationFrame(() => {\n        write(100);\n        setPhase(\"done\");\n      });\n      return () => cancelAnimationFrame(frame);\n    }\n    const span = Math.max(duration, 1);\n    let last = 0;\n    const tick = (now: number) => {\n      started.current ??= now;\n      const elapsed = now - started.current;\n      const step = last ? now - last : 16;\n      last = now;\n      const t = Math.min(elapsed / span, 1);\n      const goal = ready\n        ? 100 * easeOut(t)\n        : WAITING_CEILING * (1 - Math.exp((-3 * elapsed) / span));\n      // Glide toward the goal and never back.\n      const next = Math.max(\n        value.current,\n        value.current + (goal - value.current) * Math.min(1, step / 80)\n      );\n      if (ready && t >= 1 && next >= 99.5) {\n        write(100);\n        setPhase(\"done\");\n        return;\n      }\n      write(next);\n      frame = requestAnimationFrame(tick);\n    };\n    frame = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(frame);\n  }, [phase, ready, duration]);\n\n  // Fade out, then leave.\n  useEffect(() => {\n    if (phase !== \"done\") return;\n    const timer = setTimeout(\n      () => {\n        setPhase(\"gone\");\n        onCompleteRef.current?.();\n      },\n      prefersReducedMotion() ? 0 : Math.max(0, exitDuration)\n    );\n    return () => clearTimeout(timer);\n  }, [phase, exitDuration]);\n\n  if (phase === \"gone\") return null;\n\n  return (\n    <div\n      ref={mergedRef}\n      role=\"progressbar\"\n      aria-label={label}\n      aria-valuemin={0}\n      aria-valuemax={100}\n      aria-valuenow={progress}\n      data-slot=\"preloader\"\n      data-state={phase}\n      className={cn(\n        \"bg-background text-foreground fixed inset-0 z-50 grid place-items-center [--preloader-progress:0]\",\n        \"ease-out data-[state=done]:pointer-events-none data-[state=done]:opacity-0 motion-safe:transition-opacity\",\n        // Without scripts nothing would ever remove it.\n        \"[@media(scripting:none)]:hidden\",\n        className\n      )}\n      style={{ transitionDuration: `${exitDuration}ms`, ...style }}\n      {...props}\n    >\n      {typeof children === \"function\" ? children(progress) : children}\n    </div>\n  );\n}\n\n/**\n * A thin bar that fills with the preloader's progress, through a `scaleX`\n * transform of `--preloader-progress`. Colour the fill with\n * `[&>[data-slot=preloader-bar-fill]]:bg-…`.\n */\nfunction PreloaderBar({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      data-slot=\"preloader-bar\"\n      className={cn(\"bg-muted h-0.5 w-40 overflow-hidden rounded-full\", className)}\n      {...props}\n    >\n      <div\n        data-slot=\"preloader-bar-fill\"\n        className=\"bg-primary h-full w-full origin-left\"\n        style={{ transform: \"scaleX(var(--preloader-progress, 0))\" }}\n      />\n    </div>\n  );\n}\n\nexport { Preloader, PreloaderBar, type PreloaderProps };\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}