{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-step-loader",
  "title": "Multi Step Loader",
  "description": "Full-screen overlay that ticks through a list of steps, keeping the active one centred.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/multi-step-loader.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface LoaderStep {\n  /** What is happening during this step */\n  text: string;\n}\n\ninterface MultiStepLoaderProps\n  extends Omit<React.DialogHTMLAttributes<HTMLDialogElement>, \"title\" | \"open\" | \"onClose\"> {\n  /** Show the overlay */\n  loading: boolean;\n  /** Steps, in order */\n  steps: LoaderStep[];\n  /** Active step index (steps.length = all done); omit to auto-advance */\n  current?: number;\n  /** Milliseconds per step when auto-advancing */\n  duration?: number;\n  /** Start again after the last step instead of finishing */\n  loop?: boolean;\n  /** Visible heading, also the dialog's accessible name */\n  title?: string;\n  /** Called one `duration` after every auto-advanced step is done */\n  onComplete?: () => void;\n  /** Renders a close button and lets Escape dismiss the overlay */\n  onClose?: () => void;\n}\n\n/**\n * Full-screen progress overlay for multi-step work (deploys, sign-up\n * provisioning, imports). Open it with `loading`; it either auto-advances\n * every `duration` ms or follows a `current` index you control. Built on\n * a modal `<dialog>`, so it traps focus and sits above everything.\n */\nexport function MultiStepLoader({\n  loading,\n  steps,\n  current,\n  duration = 1600,\n  loop = false,\n  title,\n  onComplete,\n  onClose,\n  className,\n  ...props\n}: MultiStepLoaderProps) {\n  const dialogRef = useRef<HTMLDialogElement>(null);\n  const listRef = useRef<HTMLOListElement>(null);\n  const titleId = useId();\n  const [auto, setAuto] = useState(0);\n  const [wasLoading, setWasLoading] = useState(loading);\n  const [paused, setPaused] = useState(false);\n  const completeRef = useRef(onComplete);\n  useEffect(() => {\n    completeRef.current = onComplete;\n  });\n\n  // Restart from the first step each time the overlay opens.\n  if (loading !== wasLoading) {\n    setWasLoading(loading);\n    if (loading) setAuto(0);\n  }\n\n  const index = Math.min(Math.max(current ?? auto, 0), steps.length);\n  const active = Math.min(index, steps.length - 1);\n\n  // Open/close the native modal: focus, scroll lock and focus return.\n  // A layout effect, so the list below is measured while the modal is open.\n  useLayoutEffect(() => {\n    const dialog = dialogRef.current;\n    if (!dialog || !loading) return;\n    const trigger = document.activeElement as HTMLElement | null;\n    const overflow = document.body.style.overflow;\n    document.body.style.overflow = \"hidden\";\n    if (!dialog.open) dialog.showModal();\n    dialog.focus();\n    return () => {\n      dialog.close();\n      document.body.style.overflow = overflow;\n      trigger?.focus();\n    };\n  }, [loading]);\n\n  // Auto-advance (uncontrolled only). A loop holds while hovered or focused.\n  useEffect(() => {\n    if (!loading || current !== undefined || (loop && paused)) return;\n    const done = auto >= steps.length;\n    const t = setTimeout(() => {\n      if (done) completeRef.current?.();\n      else setAuto((i) => (loop ? (i + 1) % steps.length : i + 1));\n    }, duration);\n    return () => clearTimeout(t);\n  }, [loading, current, loop, paused, auto, steps.length, duration]);\n\n  // Keep the active step centred in the viewport.\n  useLayoutEffect(() => {\n    const list = listRef.current;\n    const row = list?.children[active] as HTMLElement | undefined;\n    if (!list || !row || !loading) return;\n    list.style.transform = `translateY(${-(row.offsetTop + row.offsetHeight / 2)}px)`;\n  }, [active, loading]);\n\n  const status =\n    index >= steps.length\n      ? `All ${steps.length} steps complete`\n      : `Step ${index + 1} of ${steps.length}: ${steps[index]?.text}`;\n\n  return (\n    <dialog\n      ref={dialogRef}\n      tabIndex={-1}\n      aria-labelledby={title ? titleId : undefined}\n      aria-label={title ? undefined : \"Progress\"}\n      {...props}\n      onCancel={(e) => {\n        // Escape only dismisses when the parent can react to it.\n        e.preventDefault();\n        onClose?.();\n        props.onCancel?.(e);\n      }}\n      onClose={(e) => {\n        // Browsers may force-close after repeated Escape presses: sync up.\n        if (!loading) return;\n        if (onClose) onClose();\n        else e.currentTarget.showModal();\n      }}\n      onMouseEnter={(e) => {\n        setPaused(true);\n        props.onMouseEnter?.(e);\n      }}\n      onMouseLeave={(e) => {\n        setPaused(false);\n        props.onMouseLeave?.(e);\n      }}\n      onFocus={(e) => {\n        if (e.target !== e.currentTarget) setPaused(true);\n        props.onFocus?.(e);\n      }}\n      onBlur={(e) => {\n        setPaused(false);\n        props.onBlur?.(e);\n      }}\n      className={cn(\n        \"fixed inset-0 m-0 h-dvh max-h-none w-full max-w-none place-items-center border-0 bg-background/80 p-6 text-foreground opacity-0 backdrop-blur-md outline-none transition-[opacity,display,overlay] transition-discrete duration-300 backdrop:bg-transparent open:grid open:opacity-100 motion-reduce:transition-none starting:open:opacity-0\",\n        className\n      )}\n    >\n      {onClose && (\n        <button\n          type=\"button\"\n          onClick={onClose}\n          aria-label=\"Close\"\n          className=\"absolute top-4 right-4 grid size-10 cursor-pointer place-items-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n        >\n          <svg\n            aria-hidden\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            className=\"size-5\"\n          >\n            <path d=\"M6 6l12 12M18 6 6 18\" />\n          </svg>\n        </button>\n      )}\n      <div className=\"w-full max-w-sm\">\n        {title && (\n          <h2 id={titleId} className=\"mb-2 text-center text-lg font-semibold\">\n            {title}\n          </h2>\n        )}\n        <div className=\"relative h-72 overflow-hidden [mask-image:linear-gradient(transparent,black_35%,black_65%,transparent)]\">\n          <ol\n            ref={listRef}\n            className=\"absolute inset-x-0 top-1/2 transition-transform duration-500 ease-out motion-reduce:transition-none\"\n          >\n            {steps.map((step, i) => {\n              const done = i < index;\n              const now = i === index;\n              return (\n                <li\n                  key={i}\n                  aria-current={now ? \"step\" : undefined}\n                  className={cn(\n                    \"flex items-center gap-3 py-2.5 transition-[color,opacity] duration-300 motion-reduce:transition-none\",\n                    now ? \"font-medium\" : \"text-muted-foreground\",\n                    !done && !now && \"opacity-50\"\n                  )}\n                >\n                  <span\n                    className={cn(\n                      \"grid size-6 shrink-0 place-items-center rounded-full\",\n                      done\n                        ? \"bg-brand text-brand-foreground\"\n                        : now\n                          ? \"text-brand\"\n                          : \"border-2 border-current\"\n                    )}\n                  >\n                    {(done || now) && (\n                      <svg\n                        aria-hidden\n                        viewBox=\"0 0 24 24\"\n                        fill=\"none\"\n                        stroke=\"currentColor\"\n                        strokeWidth=\"3\"\n                        strokeLinecap=\"round\"\n                        strokeLinejoin=\"round\"\n                        className={done ? \"size-3.5\" : \"size-6 motion-safe:animate-spin\"}\n                      >\n                        {done ? (\n                          <path d=\"m5 12.5 4.5 4.5L19 7.5\" />\n                        ) : (\n                          <>\n                            <circle cx=\"12\" cy=\"12\" r=\"9.5\" opacity=\"0.25\" />\n                            <path d=\"M21.5 12A9.5 9.5 0 0 0 12 2.5\" />\n                          </>\n                        )}\n                      </svg>\n                    )}\n                  </span>\n                  {step.text}\n                  {done && <span className=\"sr-only\"> (done)</span>}\n                </li>\n              );\n            })}\n          </ol>\n        </div>\n        <div aria-hidden className=\"mt-4 h-1 overflow-hidden rounded-full bg-muted\">\n          <div\n            className=\"h-full rounded-full bg-brand transition-[width] duration-500 ease-out motion-reduce:transition-none\"\n            style={{ width: `${(index / Math.max(steps.length, 1)) * 100}%` }}\n          />\n        </div>\n        <p role=\"status\" className=\"sr-only\">\n          {loading ? status : \"\"}\n        </p>\n      </div>\n    </dialog>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/multi-step-loader.tsx"
    }
  ],
  "type": "registry:ui"
}