{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dialog",
  "title": "Dialog",
  "author": "AlgorizeTech",
  "description": "A composable modal dialog with focus trapping, overlay and controlled or uncontrolled open state. Use it as the general-purpose dialog primitive.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://avaraui.com/r/copy-button.json",
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/DialogBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { X } from \"lucide-react\";\nimport React, {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { DialogContext, useDialog } from \"./dialog-context\";\n\n// ==========================================\n// 2. ROOT COMPONENT\n// ==========================================\nexport interface DialogProps {\n  children: React.ReactNode;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nexport function Dialog({\n  children,\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange,\n}: DialogProps) {\n  const isControlled = controlledOpen !== undefined;\n  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);\n  const open = isControlled ? controlledOpen : uncontrolledOpen;\n\n  // What `setOpen` reads when it fires, refreshed after every commit so the\n  // setter itself never has to be rebuilt. Identity matters here: DialogContent\n  // registers on the open-dialog stack in an effect keyed by this function, and\n  // a setter that changed on every render would re-register a dialog that is\n  // merely re-rendering — handing an outer dialog the topmost slot while a\n  // nested one is still on screen, so Escape would dismiss the wrong window.\n  const latest = useRef({ open, isControlled, onOpenChange });\n  useEffect(() => {\n    latest.current = { open, isControlled, onOpenChange };\n  });\n\n  const setOpen = useCallback(\n    (newOpen: boolean | ((prev: boolean) => boolean)) => {\n      const current = latest.current;\n      const nextState =\n        typeof newOpen === \"function\" ? newOpen(current.open) : newOpen;\n      if (!current.isControlled) setUncontrolledOpen(nextState);\n      current.onOpenChange?.(nextState);\n    },\n    [],\n  );\n\n  const value = useMemo(() => ({ open, setOpen }), [open, setOpen]);\n\n  return (\n    <DialogContext.Provider value={value}>{children}</DialogContext.Provider>\n  );\n}\n\n// ==========================================\n// 3. TRIGGER\n// ==========================================\nexport interface DialogTriggerProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  asChild?: boolean; // Note: For a real asChild implementation, you'd use Radix Slot. Here we use a generic wrapper.\n}\n\nexport function DialogTrigger({\n  children,\n  className,\n  ...props\n}: DialogTriggerProps) {\n  const { setOpen } = useDialog();\n\n  return (\n    <div\n      role=\"button\"\n      tabIndex={0}\n      onClick={() => setOpen(true)}\n      onKeyDown={(e) => {\n        if (e.key === \"Enter\" || e.key === \" \") {\n          e.preventDefault();\n          setOpen(true);\n        }\n      }}\n      className={cn(\"inline-block\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n// ==========================================\n// 3b. CLOSE BUTTON\n// ==========================================\nexport type DialogCloseProps = React.ButtonHTMLAttributes<HTMLButtonElement>;\n\nexport function DialogClose({\n  children,\n  className,\n  onClick,\n  ...props\n}: DialogCloseProps) {\n  const { setOpen } = useDialog();\n\n  return (\n    <button\n      type=\"button\"\n      onClick={(e) => {\n        onClick?.(e);\n        setOpen(false);\n      }}\n      className={className}\n      {...props}\n    >\n      {children}\n    </button>\n  );\n}\n\n// ==========================================\n// 4. CONTENT (Portal Renderer with Scroll Lock)\n// ==========================================\n\n/**\n * The panel width scale, from a compact confirm up to a full-page surface.\n *\n * `full` also drops the rounding, border and viewport-height cap that the\n * inset sizes rely on, so the panel really does fill the screen rather than\n * sitting as a very wide card inside it. Its rows are centred in that screen\n * (`content-center` overrides the panel's `content-start`) and kept to the\n * `md` column width, so the content reads as a dialog floating in the middle\n * of the page rather than a strip pinned to the top-left. Pass `className`\n * (it merges after the size) to lay the surface out edge to edge instead.\n */\nconst DIALOG_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-none h-full max-h-none rounded-none border-0 content-center justify-center grid-cols-[minmax(0,32rem)]\",\n} as const;\n\nexport type DialogSize = keyof typeof DIALOG_SIZES;\n\n/**\n * Every dialog currently on screen, outermost first.\n *\n * Escape is listened for on the document, so a dialog opened from inside\n * another one would otherwise dismiss both on a single press — each open\n * content has its own live listener. Only the entry on top of this stack\n * reacts, and its position also decides which panel paints above the rest.\n */\nconst openDialogs: symbol[] = [];\n// Dialogs subscribe to the stack so each one can read its own position.\nconst stackListeners = new Set<() => void>();\nconst subscribeToStack = (onChange: () => void) => {\n  stackListeners.add(onChange);\n  return () => {\n    stackListeners.delete(onChange);\n  };\n};\nconst notifyStack = () => stackListeners.forEach((listener) => listener());\n\nexport interface DialogContentProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  hideCloseButton?: boolean;\n  /** Panel width — `full` fills the viewport edge to edge. Defaults to `md`. */\n  size?: DialogSize;\n}\n\nexport function DialogContent({\n  className,\n  children,\n  hideCloseButton = false,\n  size = \"md\",\n  ...props\n}: DialogContentProps) {\n  const { open, setOpen } = useDialog();\n  const contentRef = useRef<HTMLDivElement>(null);\n  const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);\n  // Identity for this dialog on the stack above. A symbol rather than a count\n  // so a dialog can be removed by value, wherever it sits when it closes.\n  const [id] = useState(() => Symbol(\"dialog\"));\n  // Where this dialog sits on the stack once it has claimed a slot; 0 before\n  // that, and while server-rendering.\n  const depth = Math.max(\n    0,\n    useSyncExternalStore(\n      subscribeToStack,\n      () => openDialogs.indexOf(id),\n      () => -1,\n    ),\n  );\n\n  // Resolves the document this dialog actually lives in, which is not always\n  // the global one — previews portal components into an iframe. Resolving in a\n  // callback ref rather than reading a ref during render keeps render pure, and\n  // a null target doubles as the \"not mounted yet\" signal that guards SSR.\n  const setAnchor = useCallback((node: HTMLSpanElement | null) => {\n    setPortalTarget(node ? node.ownerDocument.body : null);\n  }, []);\n\n  // Handle Escape Key & Body Scroll Lock\n  useEffect(() => {\n    if (!open) return;\n\n    // Resolve the document/window the dialog actually lives in (supports iframe rendering).\n    const doc = contentRef.current?.ownerDocument ?? document;\n    const win = doc.defaultView ?? window;\n\n    // Claim a slot on the stack. Whatever opens last is what the user is\n    // looking at, so it takes both the Escape key and the highest layer.\n    openDialogs.push(id);\n    notifyStack();\n\n    // Lock body scroll. A nested dialog reads the lock the outer one already\n    // applied as its \"original\", so unwinding in either order is safe.\n    const originalStyle = win.getComputedStyle(doc.body).overflow;\n    doc.body.style.overflow = \"hidden\";\n\n    // Escape listener — ignored unless this is the topmost dialog.\n    const handleEscape = (e: KeyboardEvent) => {\n      if (e.key !== \"Escape\") return;\n      if (openDialogs[openDialogs.length - 1] !== id) return;\n      setOpen(false);\n    };\n    doc.addEventListener(\"keydown\", handleEscape);\n\n    return () => {\n      const index = openDialogs.indexOf(id);\n      if (index !== -1) openDialogs.splice(index, 1);\n      notifyStack();\n      doc.body.style.overflow = originalStyle;\n      doc.removeEventListener(\"keydown\", handleEscape);\n    };\n  }, [open, setOpen, portalTarget, id]);\n\n  if (!portalTarget || !open) return <span ref={setAnchor} hidden />;\n\n  return (\n    <>\n      <span ref={setAnchor} hidden />\n      {createPortal(\n    <div\n      className={cn(\n        \"fixed inset-0 z-50 flex items-center justify-center\",\n        // A full-page panel has no room for the inset gutter the others sit in.\n        size === \"full\" ? \"p-0\" : \"p-4 sm:p-6\",\n      )}\n      // Stack position, not a fixed layer: a dialog opened from inside another\n      // one has to paint above its parent's backdrop.\n      style={{ zIndex: 50 + depth * 10 }}\n    >\n      {/* Backdrop */}\n      <div\n        className=\"fixed inset-0 bg-black/30 dark:bg-black/80 backdrop-blur-sm animate-in fade-in duration-200\"\n        onClick={() => setOpen(false)}\n        aria-hidden=\"true\"\n      />\n\n      {/* Dialog Window */}\n      <div\n        ref={contentRef}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        className={cn(\n          // `content-start` is what keeps the rows at their natural height.\n          // The panel is a grid, so the moment it has a determinate height —\n          // `size=\"full\"`, or a height passed through className — the default\n          // `align-content: stretch` inflates every auto row to fill it, and a\n          // footer button stretches along with the row holding it.\n          // `grid-cols-1` is minmax(0, 1fr): an implicit auto column grows to\n          // its widest nowrap child, so a `truncate`d line — a long customer\n          // or file name — pushed the panel wide and scrolled it sideways\n          // instead of ending in an ellipsis.\n          \"relative z-50 w-full grid grid-cols-1 content-start gap-4 border border-border bg-card text-card-foreground p-6 shadow-lg rounded-xl max-h-[85vh] overflow-y-auto\",\n          DIALOG_SIZES[size],\n          \"animate-in fade-in zoom-in-95 slide-in-from-top-[5%] duration-200\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n\n        {!hideCloseButton && (\n          <button\n            onClick={() => setOpen(false)}\n            className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-card transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-muted data-[state=open]:text-muted-foreground\"\n          >\n            <X className=\"h-4 w-4\" />\n            <span className=\"sr-only\">Close</span>\n          </button>\n        )}\n      </div>\n    </div>,\n    portalTarget,\n      )}\n    </>\n  );\n}\n\n// ==========================================\n// 5. LAYOUT SUB-COMPONENTS\n// ==========================================\nexport function DialogHeader({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col space-y-1.5 text-center sm:text-left\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function DialogFooter({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function DialogTitle({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLHeadingElement>) {\n  return (\n    <h2\n      className={cn(\n        \"text-lg font-semibold leading-none tracking-tight\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function DialogDescription({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLParagraphElement>) {\n  return (\n    <p\n      className={cn(\"text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/DialogBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/dialog-context.ts",
      "content": "\"use client\";\n\nimport React, { createContext, useContext } from \"react\";\n\nconst COLOR_HEX_MAP: Record<string, string> = {\n  indigo: \"#6366f1\",\n  rose: \"#f43f5e\",\n  emerald: \"#10b981\",\n  zinc: \"#71717a\",\n  amber: \"#f59e0b\",\n  blue: \"#3b82f6\",\n};\n\nexport const getThemeStyles = (color?: string) => {\n  if (!color || color === \"default\" || color === \"primary\") {\n    return { style: {} };\n  }\n\n  const resolvedColor =\n    COLOR_HEX_MAP[color] || (color.startsWith(\"#\") ? color : undefined);\n\n  if (resolvedColor) {\n    return {\n      style: {\n        \"--primary\": resolvedColor,\n      } as React.CSSProperties,\n    };\n  }\n\n  return { style: {} };\n};\n\ninterface DialogContextType {\n  open: boolean;\n  setOpen: React.Dispatch<React.SetStateAction<boolean>>;\n  color?: string;\n  style?: React.CSSProperties;\n}\n\nexport const DialogContext = createContext<DialogContextType | undefined>(undefined);\n\nexport function useDialog() {\n  const context = useContext(DialogContext);\n  if (!context)\n    throw new Error(\"Dialog components must be used within a <Dialog> root.\");\n  return context;\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/dialog-context.ts"
    }
  ],
  "docs": "Named exports: Dialog, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, useDialog (from dialog-context.ts, which installs alongside), plus the DialogSize type. Use `defaultOpen` for uncontrolled, or `open` with `onOpenChange` to drive it — `onOpenChange` also fires on Escape and backdrop clicks, which is where you refuse a dismissal while async work is in flight. Size the panel with `size`: sm | md | lg | xl | full, where full fills the viewport and drops the rounding, border and height cap; `className` is merged after it and wins. `hideCloseButton` drops the built-in X. Dialogs nest — render one inside another's content and the inner one portals above its parent, with Escape and backdrop clicks reaching only the topmost. For a controlled overlay you drive entirely from your own state, use `modal`.",
  "categories": [
    "overlays"
  ],
  "type": "registry:component"
}
