{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dialog","type":"registry:ui","title":"Dialog","description":"Modal built on the native <dialog>: blurred backdrop, spring scale-in and fade-out, Escape to close and focus returned to the trigger.","dependencies":["lucide-react"],"registryDependencies":["utils"],"files":[{"path":"registry/ui/dialog.tsx","type":"registry:ui","content":"\"use client\";\n\nimport {\n  createContext,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ButtonHTMLAttributes,\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type MouseEvent,\n  type ReactNode,\n} from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n// Enter/exit is pure CSS: [open] + @starting-style for the entrance, and\n// `allow-discrete` on display/overlay so the native <dialog> stays painted\n// (and in the top layer) while it fades out after close().\nconst css = `\n.ui-dialog{opacity:0;scale:.95;translate:0 10px;transition:opacity .18s ease-in,scale .18s ease-in,translate .18s ease-in,display .18s allow-discrete,overlay .18s allow-discrete}\n.ui-dialog[open]{opacity:1;scale:1;translate:0 0;transition-timing-function:cubic-bezier(.2,.9,.25,1.15);transition-duration:.28s}\n@starting-style{.ui-dialog[open]{opacity:0;scale:.95;translate:0 10px}}\n.ui-dialog::backdrop{background:rgb(0 0 0/0);backdrop-filter:blur(0);transition:background .25s,backdrop-filter .25s,display .25s allow-discrete,overlay .25s allow-discrete}\n.ui-dialog[open]::backdrop{background:rgb(0 0 0/.5);backdrop-filter:blur(6px)}\n@starting-style{.ui-dialog[open]::backdrop{background:rgb(0 0 0/0);backdrop-filter:blur(0)}}\nhtml:has(.ui-dialog:modal){overflow:hidden}\n@media (prefers-reduced-motion:reduce){.ui-dialog,.ui-dialog::backdrop{transition-duration:0s!important}}\n`;\n\nconst FOCUSABLE =\n  'a[href],button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),[tabindex]:not([tabindex=\"-1\"])';\n\ntype Ctx = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  position: \"fixed\" | \"absolute\";\n  ids: { trigger: string; content: string; title: string; description: string };\n};\nconst DialogContext = createContext<Ctx | null>(null);\n\nfunction useDialog() {\n  const ctx = useContext(DialogContext);\n  if (!ctx) throw new Error(\"Dialog parts must be rendered inside <Dialog>.\");\n  return ctx;\n}\n\nexport interface DialogProps {\n  /** Controlled open state. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. */\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /**\n   * `fixed` opens a true modal (showModal: top layer, inert page, native ::backdrop).\n   * `absolute` renders inside the nearest positioned ancestor, for previews and embeds.\n   */\n  position?: \"fixed\" | \"absolute\";\n  children?: ReactNode;\n}\n\nexport function Dialog({ open: openProp, defaultOpen = false, onOpenChange, position = \"fixed\", children }: DialogProps) {\n  const [inner, setInner] = useState(defaultOpen);\n  const open = openProp ?? inner;\n  const id = useId();\n  const setOpen = (next: boolean) => {\n    if (openProp === undefined) setInner(next);\n    onOpenChange?.(next);\n  };\n  const ids = { trigger: `${id}-trigger`, content: `${id}-content`, title: `${id}-title`, description: `${id}-desc` };\n  return <DialogContext value={{ open, setOpen, position, ids }}>{children}</DialogContext>;\n}\n\nexport function DialogTrigger({ className, onClick, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {\n  const { open, setOpen, ids } = useDialog();\n  return (\n    <button\n      type=\"button\"\n      id={ids.trigger}\n      aria-haspopup=\"dialog\"\n      aria-expanded={open}\n      aria-controls={ids.content}\n      {...props}\n      onClick={(e) => {\n        onClick?.(e);\n        if (!e.defaultPrevented) setOpen(true);\n      }}\n      className={cn(\n        \"inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-border bg-card px-4 text-sm font-medium text-foreground shadow-sm outline-none transition-[background-color,scale] hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/60 active:scale-[0.97]\",\n        className,\n      )}\n    />\n  );\n}\n\nexport interface DialogContentProps extends HTMLAttributes<HTMLDialogElement> {\n  /** Render the corner close button. */\n  showClose?: boolean;\n  /** Close when the backdrop is clicked. */\n  dismissible?: boolean;\n}\n\nexport function DialogContent({\n  className,\n  children,\n  showClose = true,\n  dismissible = true,\n  onKeyDown,\n  ...props\n}: DialogContentProps) {\n  const { open, setOpen, position, ids } = useDialog();\n  const ref = useRef<HTMLDialogElement>(null);\n  const returnTo = useRef<HTMLElement | null>(null);\n  const mounted = useRef(false);\n  const downOutside = useRef(false);\n  const modal = position === \"fixed\";\n\n  useEffect(() => {\n    const d = ref.current;\n    const firstRun = !mounted.current;\n    mounted.current = true;\n    if (!d) return;\n    if (open && !d.open) {\n      returnTo.current = document.activeElement as HTMLElement | null;\n      if (modal) {\n        d.showModal(); // native: moves focus inside, makes the page inert, handles Escape\n      } else {\n        d.open = true; // attribute only: no focus steal when rendered open on page load\n        if (!firstRun) (d.querySelector<HTMLElement>(\"[autofocus]\") ?? d.querySelector<HTMLElement>(FOCUSABLE))?.focus();\n      }\n    } else if (!open && d.open) {\n      d.close();\n      const back = returnTo.current?.isConnected && returnTo.current !== document.body ? returnTo.current : document.getElementById(ids.trigger);\n      back?.focus({ preventScroll: true });\n    }\n  }, [open, modal, ids.trigger]);\n\n  const outside = (e: MouseEvent<HTMLDialogElement>) => {\n    const r = e.currentTarget.getBoundingClientRect();\n    return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;\n  };\n\n  const onKey = (e: KeyboardEvent<HTMLDialogElement>) => {\n    onKeyDown?.(e);\n    if (modal || e.defaultPrevented) return;\n    // Non-modal (absolute) mode: emulate Escape + a Tab focus trap ourselves.\n    if (e.key === \"Escape\") {\n      e.stopPropagation();\n      setOpen(false);\n    } else if (e.key === \"Tab\") {\n      const items = [...e.currentTarget.querySelectorAll<HTMLElement>(FOCUSABLE)];\n      const first = items[0];\n      const last = items[items.length - 1];\n      if (document.activeElement === (e.shiftKey ? first : last)) {\n        e.preventDefault();\n        (e.shiftKey ? last : first)?.focus();\n      }\n    }\n  };\n\n  return (\n    <>\n      <style href=\"ui-dialog\" precedence=\"default\">\n        {css}\n      </style>\n      {!modal && (\n        <div\n          aria-hidden\n          onClick={() => dismissible && setOpen(false)}\n          className={cn(\n            \"absolute inset-0 z-40 bg-black/50 backdrop-blur-[6px] transition-opacity duration-250 motion-reduce:transition-none\",\n            open ? \"opacity-100\" : \"pointer-events-none opacity-0\",\n          )}\n        />\n      )}\n      <dialog\n        {...props}\n        ref={ref}\n        id={ids.content}\n        aria-labelledby={ids.title}\n        aria-describedby={ids.description}\n        aria-modal={modal ? undefined : true}\n        onCancel={(e) => {\n          e.preventDefault(); // let state drive the close so the exit animation runs\n          setOpen(false);\n        }}\n        onClose={() => open && setOpen(false)}\n        onKeyDown={onKey}\n        onPointerDown={(e) => (downOutside.current = outside(e))}\n        onClick={(e) => {\n          if (dismissible && modal && downOutside.current && outside(e)) setOpen(false);\n          downOutside.current = false;\n        }}\n        className={cn(\n          \"ui-dialog z-50 m-auto h-fit max-h-[calc(100%-2rem)] w-[calc(100%-2rem)] max-w-md overflow-y-auto rounded-2xl border border-border bg-card p-6 text-card-foreground shadow-2xl outline-none\",\n          modal ? \"fixed inset-0\" : \"absolute inset-0\",\n          className,\n        )}\n      >\n        {children}\n        {showClose && (\n          <button\n            type=\"button\"\n            aria-label=\"Close\"\n            onClick={() => setOpen(false)}\n            className=\"absolute right-4 top-4 grid size-8 place-items-center rounded-full text-muted-foreground outline-none transition-[background-color,color,rotate] duration-200 hover:rotate-90 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60 motion-reduce:transition-none\"\n          >\n            <X className=\"size-4\" />\n          </button>\n        )}\n      </dialog>\n    </>\n  );\n}\n\nexport function DialogHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n  return <div {...props} className={cn(\"mb-5 flex flex-col gap-1.5 pr-8\", className)} />;\n}\n\nexport function DialogTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {\n  const { ids } = useDialog();\n  return <h2 id={ids.title} {...props} className={cn(\"text-lg font-semibold leading-tight tracking-tight\", className)} />;\n}\n\nexport function DialogDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {\n  const { ids } = useDialog();\n  return <p id={ids.description} {...props} className={cn(\"text-sm text-muted-foreground\", className)} />;\n}\n\nexport function DialogFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n  return <div {...props} className={cn(\"mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)} />;\n}\n\nexport function DialogClose({ className, onClick, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {\n  const { setOpen } = useDialog();\n  return (\n    <button\n      type=\"button\"\n      {...props}\n      onClick={(e) => {\n        onClick?.(e);\n        if (!e.defaultPrevented) setOpen(false);\n      }}\n      className={cn(\n        \"inline-flex h-10 items-center justify-center rounded-lg border border-border bg-transparent px-4 text-sm font-medium text-foreground outline-none transition-[background-color,scale] hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/60 active:scale-[0.97]\",\n        className,\n      )}\n    />\n  );\n}\n"}]}