{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loading-overlay",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://usva.build/r/spinner.json"
  ],
  "files": [
    {
      "path": "loading-overlay.tsx",
      "target": "components/ui/loading-overlay.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  Spinner,\n  type SpinnerSize,\n  type SpinnerTone,\n  type SpinnerVariant,\n} from \"./spinner\";\nimport { useScrollLock } from \"./use-scroll-lock\";\n\nexport type OverlayContain = \"viewport\" | \"parent\";\n\nexport interface LoadingOverlayProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /**\n   * `parent` covers the nearest positioned ancestor and locks nothing.\n   * `viewport` covers the page and locks body scroll.\n   */\n  contain?: OverlayContain;\n  label?: string;\n  blur?: boolean;\n  size?: SpinnerSize;\n  variant?: SpinnerVariant;\n  tone?: SpinnerTone;\n}\n\n/**\n * A scrim with a centered spinner.\n *\n * Defaults to `contain=\"parent\"` because that variant locks nothing, and a\n * scroll lock is the one thing here that can reach outside the component and\n * break an unrelated modal.\n */\nexport const LoadingOverlay = React.forwardRef<\n  HTMLDivElement,\n  LoadingOverlayProps\n>(\n  (\n    {\n      className,\n      contain = \"parent\",\n      label,\n      blur = true,\n      size = \"lg\",\n      variant = \"ring\",\n      tone = \"accent\",\n      ...props\n    },\n    ref,\n  ) => {\n    useScrollLock(contain === \"viewport\");\n\n    return (\n      <div\n        ref={ref}\n        data-testid=\"loading-overlay\"\n        data-contain={contain}\n        className={cn(\n          \"z-overlay grid content-center place-items-center gap-3 bg-scrim\",\n          contain === \"viewport\" ? \"fixed inset-0\" : \"absolute inset-0\",\n          blur && \"backdrop-blur-sm\",\n          className,\n        )}\n        {...props}\n      >\n        <Spinner\n          size={size}\n          variant={variant}\n          tone={tone}\n          label={label ?? \"Loading\"}\n        />\n        {label != null && (\n          <p\n            aria-hidden=\"true\"\n            className=\"animate-reveal font-mono text-xs lowercase tracking-wide text-muted\"\n          >\n            {label}\n          </p>\n        )}\n      </div>\n    );\n  },\n);\nLoadingOverlay.displayName = \"LoadingOverlay\";\n"
    },
    {
      "path": "use-scroll-lock.ts",
      "target": "components/ui/use-scroll-lock.ts",
      "type": "registry:ui",
      "content": "\"use client\";\nimport * as React from \"react\";\n\n/**\n * Module-scoped so independently mounted lockers cooperate. A plain per-instance\n * effect cannot know another component still needs the page locked.\n */\nlet holders = 0;\nlet previousOverflow = \"\";\n\nfunction acquire(): void {\n  if (holders === 0) {\n    previousOverflow = document.body.style.overflow;\n    document.body.style.overflow = \"hidden\";\n  }\n  holders += 1;\n}\n\nfunction release(): void {\n  holders -= 1;\n  if (holders <= 0) {\n    holders = 0;\n    document.body.style.overflow = previousOverflow;\n  }\n}\n\n/**\n * Locks body scroll while `active`, restoring the exact value that was there\n * before the first lock, not a hardcoded default.\n *\n * The naive version of this (`overflow = 'unset'` on cleanup) silently unlocks\n * the page when an overlay closes over a still-open modal, because the modal's\n * own lock is not consulted. Refcounting is what makes that safe.\n */\nexport function useScrollLock(active: boolean): void {\n  React.useEffect(() => {\n    if (!active) return;\n    acquire();\n    return release;\n  }, [active]);\n}\n"
    }
  ]
}