{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loading-overlay",
  "title": "Loading Overlay",
  "description": "Covers its parent while work runs, and only its parent. Waits before showing so fast answers never flash, and keeps layout and focus in place.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json",
    "https://miseui.junsu4621.workers.dev/r/skeleton.json",
    "https://miseui.junsu4621.workers.dev/r/spinner.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/loading-overlay.tsx",
      "content": "import type * as React from \"react\"\nimport { useRef } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { DEFAULT_DELAY_MS, DEFAULT_MIN_VISIBLE_MS, useDelayedOverlay } from \"@/registry/miseui/ui/loading-overlay-state\"\nimport { Skeleton } from \"@/registry/miseui/ui/skeleton\"\nimport { Spinner } from \"@/registry/miseui/ui/spinner\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype LoadingOverlayVariant = \"spinner\" | \"blur\" | \"skeleton\" | \"bar\"\ntype LoadingOverlayEffect = \"fade\"\n\nconst INSTANT = { duration: 0 } as const\nconst BAR_SECONDS = 1.2\nconst DEFAULT_SKELETON_LINES = 3\n/** A last line this short is what makes a stack of bars read as a paragraph. */\nconst LAST_LINE_CLASS = \"w-3/5\"\n\nconst COVER_CLASS: Record<LoadingOverlayVariant, string> = {\n  spinner: \"grid place-items-center bg-background/70\",\n  blur: \"grid place-items-center bg-background/30 backdrop-blur-sm [@media(prefers-reduced-transparency:reduce)]:bg-background/90 [@media(prefers-reduced-transparency:reduce)]:backdrop-blur-none\",\n  // Opaque: the skeleton replaces the content, which is stale and should not show through between its bars.\n  skeleton: \"overflow-hidden bg-background p-4\",\n  // Nothing covers the view: the old content stays readable, and pressing it does nothing while it refreshes.\n  bar: \"overflow-hidden\",\n}\n\nfunction BarTrack({ reduceMotion }: { reduceMotion: boolean }) {\n  return (\n    <span aria-hidden=\"true\" data-slot=\"loading-overlay-bar\" className=\"absolute inset-x-0 top-0 block h-0.5 overflow-hidden bg-border\">\n      {reduceMotion ? (\n        <span className=\"block h-full w-full bg-mise\" />\n      ) : (\n        <motion.span\n          className=\"block h-full w-1/3 bg-mise\"\n          initial={{ x: \"-100%\" }}\n          animate={{ x: \"300%\" }}\n          transition={{ duration: BAR_SECONDS, ease: \"easeInOut\", repeat: Infinity }}\n        />\n      )}\n    </span>\n  )\n}\n\nfunction SkeletonLines({ lines }: { lines: number }) {\n  return (\n    <span aria-hidden=\"true\" data-slot=\"loading-overlay-skeleton\" className=\"grid gap-3\">\n      {Array.from({ length: Math.max(1, lines) }, (_, index) => (\n        <Skeleton key={index} effect=\"pulse\" className={cn(\"h-4\", index === lines - 1 && lines > 1 ? LAST_LINE_CLASS : \"w-full\")} />\n      ))}\n    </span>\n  )\n}\n\ninterface CoverBodyProps {\n  variant: LoadingOverlayVariant\n  label: string\n  showLabel: boolean\n  skeleton: React.ReactNode\n  skeletonLines: number\n  reduceMotion: boolean\n}\n\nfunction CoverBody({ variant, label, showLabel, skeleton, skeletonLines, reduceMotion }: CoverBodyProps) {\n  if (variant === \"bar\") return <BarTrack reduceMotion={reduceMotion} />\n  if (variant === \"skeleton\") return <>{skeleton ?? <SkeletonLines lines={skeletonLines} />}</>\n  if (variant === \"blur\") {\n    return showLabel ? (\n      <span aria-hidden=\"true\" className=\"rounded-full border bg-background px-3 py-1 text-[13px] text-foreground shadow-xs\">\n        {label}\n      </span>\n    ) : null\n  }\n  return (\n    <span aria-hidden=\"true\" className=\"inline-flex items-center gap-2 text-[13px] text-foreground\">\n      <Spinner size=\"sm\" label=\"\" />\n      {showLabel ? label : null}\n    </span>\n  )\n}\n\ntype LoadingOverlayProps = React.ComponentProps<\"div\"> & {\n  /** The work is running. The container is marked busy at once; the cover follows after `delay`. */\n  loading: boolean\n  /** `spinner` a soft scrim with a spinner, `blur` frosts what is behind, `skeleton` replaces it with bars, `bar` a thin line along the top and content left readable. */\n  variant?: LoadingOverlayVariant\n  /** `fade` eases the cover in and out once the delay has passed. */\n  effect?: LoadingOverlayEffect\n  /** How long to wait before showing anything, in ms. A faster answer shows nothing at all. */\n  delay?: number\n  /** Shortest time the cover stays once shown, in ms, so it cannot blink. */\n  minVisible?: number\n  /** Said to assistive tech when the cover shows. */\n  label?: string\n  /** Shows the label on screen too. On by default for `blur`, which has nothing else to look at. */\n  showLabel?: boolean\n  /** Your own skeleton, matching the real content, in place of the generic lines. */\n  skeleton?: React.ReactNode\n  skeletonLines?: number\n  /** Classes for the cover, such as bg-card when the parent is a card. */\n  overlayClassName?: string\n}\n\nfunction LoadingOverlay({\n  loading,\n  variant = \"spinner\",\n  effect,\n  delay = DEFAULT_DELAY_MS,\n  minVisible = DEFAULT_MIN_VISIBLE_MS,\n  label = \"Loading\",\n  showLabel,\n  skeleton,\n  skeletonLines = DEFAULT_SKELETON_LINES,\n  overlayClassName,\n  className,\n  children,\n  ...props\n}: LoadingOverlayProps) {\n  const contentRef = useRef<HTMLDivElement>(null)\n  const shown = useDelayedOverlay({ loading, delay, minVisible, contentRef })\n  const reduceMotion = useReducedMotion() ?? false\n  const transition = effect === \"fade\" && !reduceMotion ? SPRING_DEFAULT : INSTANT\n  return (\n    <div data-slot=\"loading-overlay\" data-variant={variant} data-effect={effect} data-shown={shown ? \"true\" : \"false\"} aria-busy={loading} className={cn(\"relative min-w-0\", className)} {...props}>\n      <div ref={contentRef} data-slot=\"loading-overlay-content\" inert={shown} className={cn(\"transition-opacity duration-(--duration-fast) ease-(--ease-mise) motion-reduce:transition-none\", shown && variant === \"bar\" && \"opacity-60\")}>\n        {children}\n      </div>\n      <AnimatePresence initial={false}>\n        {shown ? (\n          <motion.div\n            data-slot=\"loading-overlay-cover\"\n            className={cn(\"absolute inset-0 z-10 rounded-[inherit]\", COVER_CLASS[variant], overlayClassName)}\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            transition={transition}\n          >\n            <CoverBody variant={variant} label={label} showLabel={showLabel ?? variant === \"blur\"} skeleton={skeleton} skeletonLines={skeletonLines} reduceMotion={reduceMotion} />\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n      {/* Stays mounted with changing text: a live region added together with its message is often not read. */}\n      <span role=\"status\" className=\"sr-only\">\n        {shown ? label : \"\"}\n      </span>\n    </div>\n  )\n}\n\nexport { LoadingOverlay, type LoadingOverlayProps, type LoadingOverlayVariant, type LoadingOverlayEffect }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/loading-overlay-state.ts",
      "content": "import type * as React from \"react\"\nimport { useEffect, useRef, useState } from \"react\"\n\n/** Below about 200 ms a wait feels instant; showing anything in that window only adds a flash. */\nconst DEFAULT_DELAY_MS = 200\n/** Once visible it stays long enough to be read as a state, not a flicker. */\nconst DEFAULT_MIN_VISIBLE_MS = 400\n\nfunction remainingVisible(shownAt: number, now: number, minVisible: number) {\n  return Math.max(0, minVisible - (now - shownAt))\n}\n\ninterface DelayedOverlayOptions {\n  loading: boolean\n  delay: number\n  minVisible: number\n  contentRef: React.RefObject<HTMLElement | null>\n}\n\n/**\n * Inert drops focus from whatever is inside the moment it applies, so the focused element has to be noted just\n * before and handed back after. Otherwise a keyboard user lands at the top of the page each time data refreshes.\n */\nfunction useDelayedOverlay({ loading, delay, minVisible, contentRef }: DelayedOverlayOptions) {\n  const [shown, setShown] = useState(false)\n  const shownAt = useRef(0)\n  const focused = useRef<HTMLElement | null>(null)\n  useEffect(() => {\n    if (loading === shown) return\n    if (loading) {\n      const timer = setTimeout(() => {\n        const active = document.activeElement\n        focused.current = active instanceof HTMLElement && contentRef.current?.contains(active) ? active : null\n        shownAt.current = Date.now()\n        setShown(true)\n      }, delay)\n      return () => clearTimeout(timer)\n    }\n    const timer = setTimeout(() => setShown(false), remainingVisible(shownAt.current, Date.now(), minVisible))\n    return () => clearTimeout(timer)\n  }, [loading, shown, delay, minVisible, contentRef])\n  useEffect(() => {\n    if (shown || !focused.current) return\n    const target = focused.current\n    focused.current = null\n    // If the person has moved on during the wait, their new place wins.\n    if (target.isConnected && (document.activeElement === document.body || document.activeElement === null)) target.focus()\n  }, [shown])\n  return shown\n}\n\nexport { DEFAULT_DELAY_MS, DEFAULT_MIN_VISIBLE_MS, remainingVisible, useDelayedOverlay }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "feedback"
  ],
  "type": "registry:ui"
}