{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"modal","type":"registry:component","title":"Modal","description":"Centered dialog on the native dialog element that fades and scales in, leaves faster than it arrives, and keeps focus inside while open.","author":"Ryan","dependencies":["clsx","lucide-react","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/modal.tsx","type":"registry:component","target":"@components/motion/modal.tsx","content":"\"use client\";\n// easeui.dev/components/motion/modal\n\nimport { X } from \"lucide-react\";\nimport { type ReactNode, useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/** Matches the exit transition below. Closing is quicker than opening. */\nconst EXIT_MS = 150;\n\nexport interface ModalProps {\n  /** Whether the modal is open. */\n  open: boolean;\n  /** Called when the modal asks to close, from Escape, the backdrop, or the close button. */\n  onOpenChange: (open: boolean) => void;\n  /** Heading that also names the dialog for screen readers. */\n  title: ReactNode;\n  /** Optional line under the title. */\n  description?: ReactNode;\n  /** Buttons along the bottom edge. */\n  footer?: ReactNode;\n  children?: ReactNode;\n  className?: string;\n}\n\n/**\n * A centered dialog built on the native dialog element, so focus stays inside,\n * the page behind is inert, and focus returns to the trigger on close. It fades\n * and scales in from 0.97, and leaves a little faster than it arrived.\n */\nexport function Modal({\n  open,\n  onOpenChange,\n  title,\n  description,\n  footer,\n  children,\n  className,\n}: ModalProps) {\n  const ref = useRef<HTMLDialogElement>(null);\n  const titleId = useId();\n  const descriptionId = useId();\n  // Drives the CSS transition. It lags one frame behind opening so the entrance can animate.\n  const [shown, setShown] = useState(false);\n\n  useEffect(() => {\n    const dialog = ref.current;\n    if (!dialog) return;\n\n    if (open) {\n      if (!dialog.open) dialog.showModal();\n      const frame = requestAnimationFrame(() => setShown(true));\n      return () => cancelAnimationFrame(frame);\n    }\n\n    setShown(false);\n    if (!dialog.open) return;\n    // Let the exit transition finish before the dialog leaves the top layer.\n    const timeout = setTimeout(() => dialog.close(), EXIT_MS);\n    return () => clearTimeout(timeout);\n  }, [open]);\n\n  // The native dialog does not stop the page behind it from scrolling.\n  useEffect(() => {\n    if (!open) return;\n    const root = document.documentElement;\n    const previous = root.style.overflow;\n    root.style.overflow = \"hidden\";\n    return () => {\n      root.style.overflow = previous;\n    };\n  }, [open]);\n\n  return (\n    <dialog\n      ref={ref}\n      aria-labelledby={titleId}\n      aria-describedby={description ? descriptionId : undefined}\n      data-shown={shown}\n      // Escape fires cancel. Close through state instead so the exit animates.\n      onCancel={(event) => {\n        event.preventDefault();\n        onOpenChange(false);\n      }}\n      onClose={() => {\n        if (open) onOpenChange(false);\n      }}\n      className=\"group fixed inset-0 m-0 h-dvh max-h-none w-screen max-w-none items-center justify-center overflow-hidden bg-transparent p-4 text-foreground backdrop:bg-transparent open:flex\"\n    >\n      <button\n        type=\"button\"\n        aria-label=\"Close\"\n        tabIndex={-1}\n        onClick={() => onOpenChange(false)}\n        className=\"absolute inset-0 cursor-default bg-black/40 opacity-0 transition-opacity duration-150 ease-out group-data-[shown=true]:opacity-100 group-data-[shown=true]:duration-200 motion-reduce:transition-none\"\n      />\n      <div\n        className={cn(\n          \"relative flex max-h-full w-full max-w-md flex-col overflow-y-auto rounded-3xl bg-background p-6 shadow-[0_0_0_1px_var(--border-strong),0_24px_60px_-20px_rgb(0_0_0/0.45)]\",\n          \"scale-[0.97] opacity-0 transition-[opacity,scale] duration-150 ease-out\",\n          \"group-data-[shown=true]:scale-100 group-data-[shown=true]:opacity-100 group-data-[shown=true]:duration-200\",\n          \"motion-reduce:transition-none\",\n          className,\n        )}\n      >\n        <div className=\"flex items-start justify-between gap-4\">\n          <div className=\"flex flex-col gap-1.5\">\n            <h2 id={titleId} className=\"text-lg font-semibold tracking-tight\">\n              {title}\n            </h2>\n            {description ? (\n              <p id={descriptionId} className=\"text-pretty text-sm text-muted-foreground\">\n                {description}\n              </p>\n            ) : null}\n          </div>\n          <button\n            type=\"button\"\n            aria-label=\"Close\"\n            onClick={() => onOpenChange(false)}\n            className=\"relative -mr-2 -mt-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors duration-150 after:absolute after:-inset-1.5 hover:bg-muted hover:text-foreground\"\n          >\n            <X className=\"h-4 w-4\" />\n          </button>\n        </div>\n        {children ? <div className=\"mt-5\">{children}</div> : null}\n        {footer ? <div className=\"mt-6 flex flex-wrap justify-end gap-2\">{footer}</div> : null}\n      </div>\n    </dialog>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}