{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dialog",
  "title": "Dialog",
  "author": "Zubeyr <https://zubeyr.dev>",
  "description": "A modal dialog and a confirm dialog: focus moves in and back out, Tab stays inside, Escape and outside clicks close, background scroll locks, nested dialogs stack, and it slides up as a sheet on phones.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/dashboard/components/dialog/dialog.tsx",
      "content": "'use client'\n\nimport { useId, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { AlertTriangle, X } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nimport { useModal, usePresence } from './modal'\n\nexport { useModal, usePresence }\n\nconst sizes = {\n  sm: 'max-w-sm',\n  md: 'max-w-lg',\n  lg: 'max-w-2xl',\n  xl: 'max-w-4xl',\n  full: 'max-w-[min(72rem,100%)]',\n}\n\nexport type DialogProps = {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  title: React.ReactNode\n  description?: React.ReactNode\n  footer?: React.ReactNode\n  size?: keyof typeof sizes\n  dismissible?: boolean\n  hideClose?: boolean\n  initialFocus?: React.RefObject<HTMLElement | null>\n  closeLabel?: string\n  className?: string\n  children?: React.ReactNode\n}\n\nexport function Dialog({\n  open,\n  onOpenChange,\n  title,\n  description,\n  footer,\n  size = 'md',\n  dismissible = true,\n  hideClose = false,\n  initialFocus,\n  closeLabel = 'Close',\n  className,\n  children,\n}: DialogProps) {\n  const titleId = useId()\n  const descriptionId = useId()\n  const { present, visible } = usePresence(open)\n  const { panel } = useModal({\n    open,\n    onClose: () => onOpenChange(false),\n    dismissible,\n    initialFocus,\n  })\n\n  if (!present || typeof document === 'undefined') return null\n\n  return createPortal(\n    <div\n      className={cn(\n        'fixed inset-0 z-[150] flex items-end justify-center p-3 transition-opacity duration-150 motion-reduce:transition-none sm:items-center sm:p-6',\n        visible ? 'opacity-100' : 'opacity-0',\n      )}\n    >\n      <div\n        aria-hidden\n        className=\"absolute inset-0 bg-black/50\"\n        onPointerDown={() => dismissible && onOpenChange(false)}\n      />\n      <div\n        ref={panel}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-labelledby={titleId}\n        aria-describedby={description ? descriptionId : undefined}\n        tabIndex={-1}\n        className={cn(\n          'relative flex max-h-[calc(100svh-1.5rem)] w-full flex-col rounded-2xl bg-card text-card-foreground shadow-(--shadow-overlay) transition-transform duration-150 ease-out outline-none motion-reduce:transition-none sm:max-h-[calc(100svh-3rem)]',\n          sizes[size],\n          visible ? 'translate-none scale-none' : 'translate-y-2 scale-[0.98]',\n          className,\n        )}\n      >\n        <header className=\"flex items-start gap-3 px-5 pt-5 pb-3\">\n          <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n            <h2 id={titleId} className=\"text-base font-semibold\">\n              {title}\n            </h2>\n            {description && (\n              <p id={descriptionId} className=\"text-sm text-muted-foreground\">\n                {description}\n              </p>\n            )}\n          </div>\n          {!hideClose && (\n            <button\n              type=\"button\"\n              aria-label={closeLabel}\n              onClick={() => onOpenChange(false)}\n              className=\"-mt-1 -mr-2 flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4\"\n            >\n              <X />\n            </button>\n          )}\n        </header>\n        {children && <div className=\"min-h-0 flex-1 overflow-y-auto px-5 pb-5\">{children}</div>}\n        {footer && (\n          <footer className=\"flex flex-wrap items-center justify-end gap-2 border-t px-5 py-3\">\n            {footer}\n          </footer>\n        )}\n      </div>\n    </div>,\n    document.body,\n  )\n}\n\nexport type ConfirmDialogProps = {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  title: React.ReactNode\n  description?: React.ReactNode\n  confirmLabel?: string\n  cancelLabel?: string\n  tone?: 'default' | 'danger'\n  onConfirm: () => void | Promise<void>\n}\n\nexport function ConfirmDialog({\n  open,\n  onOpenChange,\n  title,\n  description,\n  confirmLabel = 'Confirm',\n  cancelLabel = 'Cancel',\n  tone = 'default',\n  onConfirm,\n}: ConfirmDialogProps) {\n  const [busy, setBusy] = useState(false)\n  const confirm = async () => {\n    setBusy(true)\n    try {\n      await onConfirm()\n      onOpenChange(false)\n    } finally {\n      setBusy(false)\n    }\n  }\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(next) => !busy && onOpenChange(next)}\n      size=\"sm\"\n      dismissible={!busy}\n      hideClose\n      title={\n        <span className=\"flex items-center gap-2\">\n          {tone === 'danger' && <AlertTriangle className=\"size-4 text-destructive\" />}\n          {title}\n        </span>\n      }\n      description={description}\n      footer={\n        <>\n          <button\n            type=\"button\"\n            disabled={busy}\n            onClick={() => onOpenChange(false)}\n            className=\"h-8 rounded-lg px-3 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-50\"\n          >\n            {cancelLabel}\n          </button>\n          <button\n            type=\"button\"\n            data-autofocus\n            disabled={busy}\n            aria-busy={busy || undefined}\n            onClick={() => void confirm()}\n            className={cn(\n              'h-8 rounded-lg px-3 text-sm font-medium disabled:opacity-60',\n              tone === 'danger'\n                ? 'bg-destructive text-white hover:bg-destructive/90'\n                : 'bg-primary text-primary-foreground hover:bg-primary/90',\n            )}\n          >\n            {busy ? '…' : confirmLabel}\n          </button>\n        </>\n      }\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/dashboard/dialog/dialog.tsx"
    },
    {
      "path": "registry/dashboard/components/dialog/modal.ts",
      "content": "'use client'\n\nimport { useEffect, useRef, useState } from 'react'\n\nconst stack: symbol[] = []\nlet locks = 0\nlet saved = { overflow: '', paddingRight: '' }\n\nfunction lockScroll() {\n  if (locks++ > 0) return\n  const body = document.body\n  saved = { overflow: body.style.overflow, paddingRight: body.style.paddingRight }\n  const gap = window.innerWidth - document.documentElement.clientWidth\n  body.style.overflow = 'hidden'\n  if (gap > 0) body.style.paddingRight = `${gap}px`\n}\n\nfunction unlockScroll() {\n  if (--locks > 0) return\n  document.body.style.overflow = saved.overflow\n  document.body.style.paddingRight = saved.paddingRight\n}\n\nconst FOCUSABLE =\n  'a[href], button:not([disabled]), input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]), [contenteditable=\"true\"]'\n\nexport function usePresence(open: boolean, duration = 180) {\n  const [present, setPresent] = useState(open)\n  const [visible, setVisible] = useState(false)\n  useEffect(() => {\n    if (open) {\n      const a = requestAnimationFrame(() => {\n        setPresent(true)\n        requestAnimationFrame(() => setVisible(true))\n      })\n      return () => cancelAnimationFrame(a)\n    }\n    const a = requestAnimationFrame(() => setVisible(false))\n    const timer = setTimeout(() => setPresent(false), duration)\n    return () => {\n      cancelAnimationFrame(a)\n      clearTimeout(timer)\n    }\n  }, [open, duration])\n  return { present: present || open, visible: visible && open }\n}\n\nexport function useModal({\n  open,\n  onClose,\n  dismissible = true,\n  initialFocus,\n}: {\n  open: boolean\n  onClose: () => void\n  dismissible?: boolean\n  initialFocus?: React.RefObject<HTMLElement | null>\n}) {\n  const panel = useRef<HTMLDivElement>(null)\n  const close = useRef(onClose)\n\n  useEffect(() => {\n    close.current = onClose\n  }, [onClose])\n\n  useEffect(() => {\n    if (!open) return\n    const id = Symbol('modal')\n    stack.push(id)\n    lockScroll()\n    const previous = document.activeElement as HTMLElement | null\n    const frame = requestAnimationFrame(() => {\n      const target =\n        initialFocus?.current ??\n        panel.current?.querySelector<HTMLElement>('[autofocus], [data-autofocus]') ??\n        panel.current?.querySelector<HTMLElement>(FOCUSABLE) ??\n        panel.current\n      target?.focus()\n    })\n\n    const onEscape = (event: KeyboardEvent) => {\n      if (event.key !== 'Escape' || !dismissible || stack[stack.length - 1] !== id) return\n      event.stopPropagation()\n      close.current()\n    }\n    const onKey = (event: KeyboardEvent) => {\n      if (stack[stack.length - 1] !== id) return\n      if (event.key !== 'Tab' || !panel.current) return\n      const items = [...panel.current.querySelectorAll<HTMLElement>(FOCUSABLE)].filter(\n        (el) => el.offsetParent !== null || el === document.activeElement,\n      )\n      if (!items.length) {\n        event.preventDefault()\n        return\n      }\n      const first = items[0]\n      const last = items[items.length - 1]\n      if (event.shiftKey && document.activeElement === first) {\n        event.preventDefault()\n        last.focus()\n      } else if (!event.shiftKey && document.activeElement === last) {\n        event.preventDefault()\n        first.focus()\n      }\n    }\n    document.addEventListener('keydown', onKey, true)\n    document.addEventListener('keydown', onEscape)\n\n    return () => {\n      cancelAnimationFrame(frame)\n      document.removeEventListener('keydown', onKey, true)\n      document.removeEventListener('keydown', onEscape)\n      stack.splice(stack.indexOf(id), 1)\n      unlockScroll()\n      if (previous?.isConnected) previous.focus()\n    }\n  }, [open, dismissible, initialFocus])\n\n  return { panel }\n}\n",
      "type": "registry:component",
      "target": "components/dashboard/dialog/modal.ts"
    }
  ],
  "cssVars": {
    "light": {
      "shadow-overlay": "0 16px 40px rgb(0 0 0 / 0.12), 0 0 0 1px rgb(0 0 0 / 0.08)"
    },
    "dark": {
      "shadow-overlay": "0 16px 40px rgb(0 0 0 / 0.5), 0 0 0 1px #0e0e0e"
    }
  },
  "meta": {
    "scenario": "dashboard",
    "kind": "components"
  },
  "categories": [
    "dashboard",
    "overlays"
  ],
  "type": "registry:component"
}