{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dialog",
  "type": "registry:ui",
  "title": "Dialog",
  "description": "A focused surface for decisions that need attention.",
  "categories": [
    "disclosure"
  ],
  "dependencies": [
    "@radix-ui/react-dialog",
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/utils.json",
    "https://irisui.dev/r/motion-tokens.json"
  ],
  "files": [
    {
      "path": "src/registry/components/dialog/dialog.tsx",
      "content": "\"use client\";\n\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { createContext, useCallback, useContext, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type { Transition } from \"motion/react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\nimport { motionTokens } from \"../lib/motion-tokens\";\n\n/* While the layers leave, clicks pass through, so the trigger can reopen the dialog mid-exit. Radix sets pointer-events inline on the overlay. */\nconst overlayClass =\n  \"fixed inset-0 z-50 bg-[oklch(10%_0_0/.46)] backdrop-blur-[7px] data-[state=closed]:pointer-events-none!\";\n/* Centered with auto margins (like a native modal dialog) so transform stays free for the entrance spring. */\nconst contentClass =\n  \"fixed inset-0 z-51 m-auto h-fit max-h-[calc(100dvh_-_var(--space-8))] w-[min(calc(100vw_-_var(--space-8)),440px)] overflow-auto rounded-surface border border-line bg-surface text-ink shadow-floating focus:outline-none data-[state=closed]:pointer-events-none!\";\n/* Motion animates both layers. These keyframes only run when DialogContent sits under a bare Radix root. */\nconst overlayKeyframes =\n  \"data-[state=closed]:animate-dialog-overlay-out data-[state=open]:animate-dialog-overlay-in motion-reduce:data-[state=closed]:animate-dialog-overlay-fade-out motion-reduce:data-[state=open]:animate-dialog-fade-in\";\nconst contentKeyframes =\n  \"data-[state=closed]:animate-dialog-content-out data-[state=open]:animate-dialog-content-in motion-reduce:data-[state=closed]:animate-dialog-content-fade-out motion-reduce:data-[state=open]:animate-dialog-fade-in\";\n\n/** Mirrors the open state so the content can stay mounted while it animates out, and retarget mid-flight if it is reopened or closed early. */\nconst OpenContext = createContext<boolean | null>(null);\n\nexport function Dialog({\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  ...props\n}: ComponentPropsWithoutRef<typeof DialogPrimitive.Root>) {\n  const [uncontrolled, setUncontrolled] = useState(defaultOpen);\n  const open = openProp ?? uncontrolled;\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (openProp === undefined) setUncontrolled(next);\n      onOpenChange?.(next);\n    },\n    [openProp, onOpenChange],\n  );\n  return (\n    <OpenContext.Provider value={open}>\n      <DialogPrimitive.Root {...props} open={open} onOpenChange={setOpen} />\n    </OpenContext.Provider>\n  );\n}\n\nexport const DialogTrigger = DialogPrimitive.Trigger;\nexport const DialogClose = DialogPrimitive.Close;\n\nexport interface DialogContentProps extends ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {\n  title: string;\n  description?: string;\n  children: ReactNode;\n}\n\nconst fade: Transition = { duration: motionTokens.duration.instant };\nconst leave: Transition = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };\n\n/** When the title or description changes while open, the new copy rises in and the old copy leaves upward. */\nfunction SwapText({ text, className }: { text: string; className?: string }) {\n  const reduced = useReducedMotion();\n  return (\n    <AnimatePresence mode=\"popLayout\" initial={false}>\n      <motion.span\n        key={text}\n        className={cn(\"block\", className)}\n        initial={reduced ? false : { opacity: 0, y: \"0.3em\", filter: `blur(${motionTokens.blur.soft}px)` }}\n        animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n        exit={\n          reduced\n            ? { opacity: 0, transition: { duration: 0 } }\n            : {\n                opacity: 0,\n                y: \"-0.3em\",\n                filter: `blur(${motionTokens.blur.subtle}px)`,\n                transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] },\n              }\n        }\n        transition={{ duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }}\n      >\n        {text}\n      </motion.span>\n    </AnimatePresence>\n  );\n}\n\nexport function DialogContent({\n  title,\n  description,\n  children,\n  className,\n  onPointerDownOutside,\n  ...props\n}: DialogContentProps) {\n  const open = useContext(OpenContext);\n  const reduced = useReducedMotion();\n  // When the open state last changed. Radix waits for the click before treating a press as outside, and a press on the trigger\n  // while the dialog leaves reopens it first, so that press must not close it again.\n  const change = useRef({ open, at: 0 });\n  useLayoutEffect(() => {\n    change.current = { open, at: performance.now() };\n  }, [open]);\n  const pressOutside: DialogContentProps[\"onPointerDownOutside\"] = (event) => {\n    onPointerDownOutside?.(event);\n    if (open !== null && (!change.current.open || event.detail.originalEvent.timeStamp < change.current.at))\n      event.preventDefault();\n  };\n  const classes = cn(contentClass, className);\n  const inner = (\n    <>\n      <div className=\"flex items-start justify-between gap-5 border-b border-line p-6\">\n        <div>\n          {/* Line height sits on the text itself, so a page-level heading reset cannot shrink the title and pull the close button off its line. */}\n          <DialogPrimitive.Title className=\"relative m-0 font-body text-lg leading-body font-medium tracking-body [overflow-wrap:anywhere]\">\n            <SwapText text={title} className=\"leading-body\" />\n          </DialogPrimitive.Title>\n          {description ? (\n            <DialogPrimitive.Description className=\"relative mt-2 mb-0 text-sm leading-body text-ink-2 [overflow-wrap:anywhere]\">\n              <SwapText text={description} />\n            </DialogPrimitive.Description>\n          ) : null}\n        </div>\n        {/* A compact icon button: quick press, spring release. The negative margin centres it on the title's first line. */}\n        <DialogPrimitive.Close\n          className=\"my-[calc((var(--text-lg)_*_var(--leading-body)_-_var(--space-8))_/_2)] grid size-8 flex-none cursor-pointer place-items-center rounded-button border border-line bg-[color-mix(in_oklab,var(--hover)_50%,transparent)] text-ink-2 [transition:background-color_var(--duration-fast)_var(--ease-standard),color_var(--duration-fast)_var(--ease-standard),transform_var(--duration-spring)_var(--ease-spring)] hover:bg-hover hover:text-ink active:[transform:scale(.96)] active:[transition:background-color_var(--duration-fast)_var(--ease-standard),color_var(--duration-fast)_var(--ease-standard),transform_var(--duration-instant)_var(--ease-standard)] motion-reduce:transition-none motion-reduce:active:transform-none motion-reduce:active:transition-none\"\n          aria-label=\"Close dialog\"\n        >\n          <X size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n        </DialogPrimitive.Close>\n      </div>\n      <div className=\"p-6 text-sm leading-body\">{children}</div>\n    </>\n  );\n  // Under a bare Radix root the open state is unknown here, so CSS keyframes keyed off data-state animate the layers instead.\n  if (open === null)\n    return (\n      <DialogPrimitive.Portal>\n        <DialogPrimitive.Overlay className={cn(overlayClass, overlayKeyframes)} />\n        <DialogPrimitive.Content\n          {...props}\n          onPointerDownOutside={pressOutside}\n          className={cn(classes, contentKeyframes)}\n        >\n          {inner}\n        </DialogPrimitive.Content>\n      </DialogPrimitive.Portal>\n    );\n  // The overlay fades while the dialog rises 8px and scales up on a spring. Closing is shorter and quieter, and starts from wherever the entrance is.\n  return (\n    <AnimatePresence>\n      {open && (\n        <DialogPrimitive.Portal key=\"dialog\" forceMount>\n          <DialogPrimitive.Overlay asChild forceMount>\n            <motion.div\n              className={overlayClass}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0, transition: reduced ? fade : leave }}\n              transition={\n                reduced ? fade : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }\n              }\n            />\n          </DialogPrimitive.Overlay>\n          <DialogPrimitive.Content {...props} onPointerDownOutside={pressOutside} asChild forceMount>\n            <motion.div\n              className={classes}\n              initial={reduced ? { opacity: 0 } : { opacity: 0, y: 8, scale: 0.96 }}\n              animate={{ opacity: 1, y: 0, scale: 1 }}\n              exit={reduced ? { opacity: 0, transition: fade } : { opacity: 0, y: 4, scale: 0.98, transition: leave }}\n              transition={\n                reduced\n                  ? fade\n                  : {\n                      default: motionTokens.spring.smooth,\n                      opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] },\n                    }\n              }\n            >\n              {inner}\n            </motion.div>\n          </DialogPrimitive.Content>\n        </DialogPrimitive.Portal>\n      )}\n    </AnimatePresence>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/iris/dialog/dialog.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/dialog",
  "meta": {
    "kind": "component",
    "group": "Overlays & disclosure",
    "export": "Dialog",
    "tags": [
      "modal",
      "overlay"
    ],
    "docs": "https://irisui.dev/components/dialog"
  }
}
