{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dialog","type":"registry:ui","title":"Dialog","description":"A React modal dialog for focused tasks: a spring-scaled card on desktop and a bottom sheet on phones, with nesting and scrolling.","dependencies":["@radix-ui/react-dialog@^1.1.23","@radix-ui/react-visually-hidden@^1.2.11","lucide-react@^1.47.0","motion@^12.43.0"],"registryDependencies":["https://wingo-ui.com/r/utils.json","https://wingo-ui.com/r/button.json","https://wingo-ui.com/r/drawer.json","https://wingo-ui.com/r/motion.json","https://wingo-ui.com/r/tones.json","https://wingo-ui.com/r/use-controllable-state.json","https://wingo-ui.com/r/use-media-query.json","https://wingo-ui.com/r/use-scroll-edges.json","https://wingo-ui.com/r/use-shortcut.json","https://wingo-ui.com/r/ui-config.json"],"files":[{"path":"components/ui/dialog.tsx","type":"registry:ui","content":"\"use client\";\n\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type Ref,\n  type RefCallback,\n  type ReactNode,\n} from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport * as VisuallyHidden from \"@radix-ui/react-visually-hidden\";\nimport { X } from \"lucide-react\";\nimport { AnimatePresence, animate, motion, useMotionValue, type MotionStyle } from \"motion/react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DrawerBody,\n  DrawerContent,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerRoot,\n  DrawerTitle,\n  type DrawerRadius,\n} from \"@/components/ui/drawer\";\nimport { useControllableState } from \"@/hooks/use-controllable-state\";\nimport { useIsMobile } from \"@/hooks/use-media-query\";\nimport { fadeMaskStyle, useScrollEdges } from \"@/hooks/use-scroll-edges\";\nimport { useShortcut, useShortcutLabel } from \"@/hooks/use-shortcut\";\nimport { springs, useMotionPreset, usePrefersReducedMotion } from \"@/lib/motion\";\nimport { colorVars, toneClasses, type Tone } from \"@/lib/tones\";\nimport { useDefaults } from \"@/lib/ui-config\";\nimport { cn } from \"@/lib/utils\";\n\nexport type DialogSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\nexport type DialogRadius = \"none\" | \"sm\" | \"md\" | \"lg\" | \"full\";\n// inside: header and footer stay put and the body scrolls. outside: the dialog grows and the page behind scrolls\nexport type DialogScrollBehavior = \"inside\" | \"outside\";\nexport type DialogAlign = \"start\" | \"center\";\nexport type DialogSlot =\n  | \"overlay\"\n  | \"content\"\n  | \"header\"\n  | \"icon\"\n  | \"title\"\n  | \"description\"\n  | \"body\"\n  | \"footer\"\n  | \"close\";\nexport type { Tone as DialogTone };\n\n// ---------------------------------------------------------------- classes\n\nconst OVERLAY = \"fixed inset-0\";\nconst OVERLAY_ROOT = \"z-overlay bg-black/40 dark:bg-black/60\";\n// a nested dialog's backdrop sits above its parent card (same layer, later in the dom) and dims it a little less,\n// since the parent's own backdrop is still there. picked instead of merged: cn does not know z-overlay and z-modal clash\nconst OVERLAY_NESTED = \"z-modal bg-black/25 dark:bg-black/45\";\n// scrollBehavior=\"outside\": the backdrop is the scroller and the card sits in it, centered while it fits\nconst OVERLAY_SCROLL = \"grid place-items-center overflow-y-auto overscroll-contain px-4 py-8 sm:py-12\";\n\nconst SURFACE =\n  \"z-modal flex flex-col gap-5 bg-popover p-6 text-popover-foreground shadow-modal outline-hidden\";\n// tailwind v4 centers with the translate property, which composes with motion's transform\nconst CENTERED =\n  \"fixed top-1/2 left-1/2 max-h-[min(85dvh,720px)] w-[calc(100%-32px)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain\";\n// 16px from every edge, clear of the notch and the home indicator\nconst FULL =\n  \"fixed inset-4 top-[max(1rem,env(safe-area-inset-top))] bottom-[max(1rem,env(safe-area-inset-bottom))] overflow-y-auto overscroll-contain\";\nconst IN_OVERLAY = \"relative w-full\";\n\nconst WIDTHS: Record<DialogSize, string> = {\n  sm: \"max-w-[400px]\",\n  md: \"max-w-[520px]\",\n  lg: \"max-w-[720px]\",\n  xl: \"max-w-[960px]\",\n  full: \"\",\n};\n\n// dialogs default to lg (24px), like every large surface\nconst SURFACE_RADII: Record<DialogRadius, string> = {\n  none: \"rounded-none\",\n  sm: \"rounded-[12px]\",\n  md: \"rounded-2xl\",\n  lg: \"rounded-3xl\",\n  full: \"rounded-3xl\",\n};\n\n// the icon tile follows the surface, so a square dialog gets a square tile and full a round one\nconst TILE_RADII: Record<DialogRadius, string> = {\n  none: \"rounded-none\",\n  sm: \"rounded-[8px]\",\n  md: \"rounded-[12px]\",\n  lg: \"rounded-2xl\",\n  full: \"rounded-full\",\n};\n\n// the phone sheet's corners: 0 / 16 / 24 / 28px\nconst SHEET_RADII: Record<DialogRadius, DrawerRadius> = { none: \"none\", sm: \"sm\", md: \"md\", lg: \"lg\", full: \"lg\" };\n\n// a hairline centered in the 20px gap next to the body, full width (-inset-x-6 cancels the card's padding); it fades\n// in while the body has more to scroll on that side, so nothing shifts\nconst HEADER =\n  \"relative flex shrink-0 flex-col gap-1.5 after:pointer-events-none after:absolute after:-inset-x-6 after:-bottom-2.5 after:h-px after:bg-foreground/10 after:opacity-0 after:transition-opacity after:duration-fast after:ease-smooth data-scrolled:after:opacity-100\";\n\nconst TITLE = \"text-lg leading-7 font-semibold tracking-tight text-balance\";\nconst DESCRIPTION = \"text-sm text-muted-foreground text-pretty\";\n\n// -my-1 py-1 gives focus rings at the top and bottom of the scroller room without moving anything. focused from the\n// keyboard it drops the edge fade, so its own ring is not faded out\nconst BODY =\n  \"-mx-6 -my-1 min-h-0 flex-1 overflow-y-auto overscroll-contain rounded-[12px] px-6 py-1 outline-hidden focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-ring focus-visible:mask-none!\";\nconst BODY_OUTSIDE = \"-mx-6 px-6\";\nconst TABBABLE = 'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex=\"-1\"]), [contenteditable]';\n\n// primary action last in the dom: on the right in a row, on top when stacked\nconst FOOTER =\n  \"relative flex shrink-0 flex-col-reverse gap-2 sm:flex-row sm:justify-end before:pointer-events-none before:absolute before:-inset-x-6 before:-top-2.5 before:h-px before:bg-foreground/10 before:opacity-0 before:transition-opacity before:duration-fast before:ease-smooth data-overflow:before:opacity-100\";\n\n// on the phone sheet: stacked full width with the primary on top, and 48px buttons (size lg) whatever size was passed\nconst SHEET_FOOTER =\n  \"flex-col-reverse [&>a]:h-12 [&>a]:px-5 [&>a]:text-[15px] [&>button]:h-12 [&>button]:px-5 [&>button]:text-[15px]\";\n// the drawer keys its spacing off its own slot names; these stand in for the renamed dialog-* parts\nconst SHEET_BODY = \"[[data-slot=dialog-title]+&]:pt-3 [&:has(~[data-slot=dialog-footer])]:pb-4!\";\n\nconst ICON = \"flex size-10 shrink-0 items-center justify-center bg-(--tone-soft) text-(--tone-soft-foreground) [&_svg:not([class*='size-'])]:size-5\";\n\n// what screen readers hear when a dialog has no title at all (always pass a real one)\nconst FALLBACK_TITLE = \"Dialog\";\n\n// a card that refuses to close gives a small bump instead, so a click outside is answered\nconst BUMP = [1, 1.02, 1];\n\nconst known = <T extends string>(map: Record<T, unknown>, value: T, fallback: T): T =>\n  Object.hasOwn(map, value) ? value : fallback;\n\nconst filled = (node: ReactNode) => node !== undefined && node !== null && node !== false && node !== \"\";\n\nfunction assignRef<T>(ref: Ref<T> | undefined, node: T | null): () => void {\n  if (typeof ref === \"function\") {\n    const cleanup = ref(node);\n    return typeof cleanup === \"function\" ? cleanup : () => ref(null);\n  }\n  if (ref) ref.current = node;\n  return () => {\n    if (ref) ref.current = null;\n  };\n}\n\nfunction useMergedRef<T>(a: Ref<T> | undefined, b: Ref<T> | undefined): RefCallback<T> {\n  return useCallback(\n    (node: T | null) => {\n      const cleanups = [assignRef(a, node), assignRef(b, node)];\n      return () => cleanups.forEach((cleanup) => cleanup());\n    },\n    [a, b],\n  );\n}\n\n// ---------------------------------------------------------------- context\n\ntype DialogContextValue = {\n  // below 768px with responsive on: every part renders the drawer's sheet part instead\n  sheet: boolean;\n  open: boolean;\n  // sets the state directly, also while not dismissible (close buttons, actions)\n  setOpen: (open: boolean) => void;\n  dismissible: boolean;\n  modal: boolean;\n  shortcut: string | undefined;\n  container: HTMLElement | null | undefined;\n};\n\nconst DialogContext = createContext<DialogContextValue | null>(null);\n\nfunction useDialogContext(part: string) {\n  const context = useContext(DialogContext);\n  if (!context) throw new Error(`<${part}> must be used inside <DialogRoot> or <Dialog>`);\n  return context;\n}\n\ntype ContentContextValue = {\n  radius: DialogRadius;\n  outside: boolean;\n  // a close button sits in the top corner, so the header keeps clear of it\n  close: boolean;\n  // the body has more content above / below (header and footer hairlines)\n  edges: { top: boolean; bottom: boolean };\n  setEdges: (top: boolean, bottom: boolean) => void;\n  classNames: Partial<Record<DialogSlot, string>> | undefined;\n};\n\nconst ContentContext = createContext<ContentContextValue | null>(null);\n\nfunction useContentContext(part: string) {\n  const context = useContext(ContentContext);\n  if (!context) throw new Error(`<${part}> must be used inside <DialogContent>`);\n  return context;\n}\n\n// the dialog a nested one opens from: it recedes while a child is open\ntype NestValue = { push: () => void; release: () => void };\nconst NestContext = createContext<NestValue | null>(null);\n\n// the nearest dialog's state, e.g. to close it after a form saves or to size buttons for the phone sheet\nexport function useDialog() {\n  const { open, setOpen, sheet } = useDialogContext(\"useDialog\");\n  return { open, setOpen, sheet };\n}\n\n// ---------------------------------------------------------------- root\n\nexport type DialogRootProps = {\n  // controlled open state; pair it with onOpenChange\n  open?: boolean;\n  // starting state while uncontrolled\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  // false: outside clicks, Escape and the sheet drag do not close it (the close button and actions still do)\n  dismissible?: boolean;\n  // a bottom sheet below 768px\n  responsive?: boolean;\n  // false: no backdrop, no focus trap, the page stays usable\n  modal?: boolean;\n  // a key combo in mod+k notation that toggles the dialog from anywhere on the page\n  shortcut?: string;\n  // portal target, document.body by default\n  container?: HTMLElement | null;\n  children?: ReactNode;\n};\n\nexport function DialogRoot(dialogRootProps: DialogRootProps) {\n  const {\n    open: openProp,\n    defaultOpen = false,\n    onOpenChange,\n    dismissible = true,\n    responsive = true,\n    modal = true,\n    shortcut,\n    container,\n    children,\n  } = useDefaults(\"dialog\", dialogRootProps);\n  // the state lives here, so crossing 768px while open swaps the dialog for the sheet and keeps it open\n  const [open, setOpen] = useControllableState({ value: openProp, defaultValue: defaultOpen, onChange: onOpenChange });\n  const isMobile = useIsMobile();\n  const sheet = responsive && isMobile;\n  const parent = useContext(NestContext);\n\n  // radix asks to close on Escape and outside clicks; a dialog that is not dismissible stays\n  const requestOpen = useCallback(\n    (next: boolean) => {\n      if (!next && !dismissible) return;\n      setOpen(next);\n    },\n    [dismissible, setOpen],\n  );\n\n  // the shortcut opens it, and closes it again unless it is not dismissible\n  useShortcut(shortcut, () => setOpen((current) => !current || !dismissible));\n\n  // the dialog this one opened from steps back while it is open\n  useEffect(() => {\n    if (!open || !parent) return;\n    parent.push();\n    return () => parent.release();\n  }, [open, parent]);\n\n  const value = useMemo<DialogContextValue>(\n    () => ({ sheet, open, setOpen, dismissible, modal, shortcut, container }),\n    [sheet, open, setOpen, dismissible, modal, shortcut, container],\n  );\n\n  return (\n    <DialogContext.Provider value={value}>\n      {sheet ? (\n        // the drawer nests on its own inside another sheet, like ios; its shortcut is bound here instead\n        <DrawerRoot\n          open={open}\n          defaultOpen={defaultOpen}\n          onOpenChange={setOpen}\n          dismissible={dismissible}\n          modal={modal}\n          container={container}\n        >\n          {children}\n        </DrawerRoot>\n      ) : (\n        <DialogPrimitive.Root open={open} onOpenChange={requestOpen} modal={modal}>\n          {children}\n        </DialogPrimitive.Root>\n      )}\n    </DialogContext.Provider>\n  );\n}\n\n// ---------------------------------------------------------------- trigger and close\n\nexport type DialogTriggerProps = ComponentProps<typeof DialogPrimitive.Trigger>;\n\n// vaul runs on radix dialog, so the same trigger and close work in both modes\nexport function DialogTrigger(props: DialogTriggerProps) {\n  const { shortcut } = useDialogContext(\"DialogTrigger\");\n  const { aria } = useShortcutLabel(shortcut);\n  return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" aria-keyshortcuts={aria} {...props} />;\n}\n\nexport type DialogCloseProps = ComponentProps<typeof DialogPrimitive.Close>;\n\nexport function DialogClose({ onClick, ...props }: DialogCloseProps) {\n  const { dismissible, setOpen } = useDialogContext(\"DialogClose\");\n  return (\n    <DialogPrimitive.Close\n      data-slot=\"dialog-close\"\n      onClick={(event) => {\n        onClick?.(event);\n        // the root refuses close requests while not dismissible; a close button closes anyway\n        if (!event.defaultPrevented && !dismissible) setOpen(false);\n      }}\n      {...props}\n    />\n  );\n}\n\n// ---------------------------------------------------------------- content\n\nexport type DialogContentProps = Omit<ComponentProps<typeof DialogPrimitive.Content>, \"asChild\" | \"forceMount\"> & {\n  // max width 400 / 520 / 720 / 960px; full keeps 16px from every edge (a full-height sheet on phones)\n  size?: DialogSize;\n  radius?: DialogRadius;\n  // inside: header and footer stay, the body scrolls. outside: the dialog grows and the backdrop scrolls\n  scrollBehavior?: DialogScrollBehavior;\n  // the round close button in the top corner\n  showClose?: boolean;\n  // aria-label of the close button\n  closeLabel?: string;\n  // portal target; defaults to the root's container, then document.body\n  container?: HTMLElement | null;\n  classNames?: Partial<Record<DialogSlot, string>>;\n};\n\nexport function DialogContent(dialogContentProps: DialogContentProps) {\n  const {\n    size = \"md\",\n    radius = \"lg\",\n    scrollBehavior = \"inside\",\n    showClose = true,\n    closeLabel = \"Close\",\n    container: containerProp,\n    classNames,\n    className,\n    style,\n    children,\n    ref,\n    onEscapeKeyDown,\n    onPointerDownOutside,\n    ...props\n  } = useDefaults(\"dialog-content\", dialogContentProps);\n  const dialog = useDialogContext(\"DialogContent\");\n  const { sheet, open, dismissible, modal } = dialog;\n  const container = containerProp === undefined ? dialog.container : containerProp;\n  const safeSize = known(WIDTHS, size, \"md\");\n  const safeRadius = known(SURFACE_RADII, radius, \"lg\");\n  // a full dialog and the sheet always scroll inside; outside needs the backdrop to scroll\n  const outside = scrollBehavior === \"outside\" && safeSize !== \"full\" && modal && !sheet;\n  const nested = useContext(NestContext) !== null;\n  const reduced = usePrefersReducedMotion();\n  const overlayPreset = useMotionPreset(\"overlay\");\n  const dialogPreset = useMotionPreset(\"dialog\");\n\n  const [edges, setEdgesState] = useState({ top: false, bottom: false });\n  const setEdges = useCallback((top: boolean, bottom: boolean) => {\n    setEdgesState((prev) => (prev.top === top && prev.bottom === bottom ? prev : { top, bottom }));\n  }, []);\n  const contentValue = useMemo<ContentContextValue>(\n    () => ({ radius: safeRadius, outside, close: showClose, edges, setEdges, classNames }),\n    [safeRadius, outside, showClose, edges, setEdges, classNames],\n  );\n\n  // dialogs opened from inside this one\n  const [childCount, setChildren] = useState(0);\n  const nest = useMemo<NestValue>(\n    () => ({ push: () => setChildren((n) => n + 1), release: () => setChildren((n) => Math.max(0, n - 1)) }),\n    [],\n  );\n  const childOpen = childCount > 0;\n\n  const bump = useMotionValue(1);\n  const refuse = () => {\n    if (!reduced) animate(bump, BUMP, { duration: 0.3, ease: \"easeInOut\" });\n  };\n\n  const closeButton = showClose ? (\n    <DialogClose asChild>\n      <Button\n        variant=\"ghost\"\n        size=\"icon-sm\"\n        radius=\"full\"\n        aria-label={closeLabel}\n        className={cn(\n          \"absolute end-4\",\n          // on the sheet it lines up with the title under the handle (no handle while not dismissible)\n          sheet && dismissible ? \"top-6\" : \"top-4\",\n          classNames?.close,\n        )}\n      >\n        <X />\n      </Button>\n    </DialogClose>\n  ) : null;\n\n  const inner = (\n    <ContentContext.Provider value={contentValue}>\n      {/* on the sheet vaul scales the parent itself; the count still marks data-nested / data-child-open */}\n      <NestContext.Provider value={nest}>{children}</NestContext.Provider>\n    </ContentContext.Provider>\n  );\n\n  if (sheet) {\n    return (\n      <DrawerContent\n        ref={ref}\n        data-slot=\"dialog-content\"\n        data-size={safeSize}\n        data-nested={nested || undefined}\n        data-child-open={childOpen || undefined}\n        height={safeSize === \"full\" ? \"full\" : \"auto\"}\n        radius={SHEET_RADII[safeRadius]}\n        container={container}\n        classNames={{ overlay: classNames?.overlay }}\n        className={cn(classNames?.content, className)}\n        style={style}\n        onEscapeKeyDown={onEscapeKeyDown}\n        onPointerDownOutside={onPointerDownOutside}\n        {...props}\n      >\n        {inner}\n        {closeButton}\n      </DrawerContent>\n    );\n  }\n\n  const card = (\n    <DialogPrimitive.Content\n      forceMount\n      asChild\n      onEscapeKeyDown={(event) => {\n        onEscapeKeyDown?.(event);\n        if (dismissible || event.defaultPrevented) return;\n        event.preventDefault();\n        refuse();\n      }}\n      onPointerDownOutside={(event) => {\n        onPointerDownOutside?.(event);\n        if (dismissible || event.defaultPrevented) return;\n        event.preventDefault();\n        refuse();\n      }}\n      {...props}\n    >\n      <motion.div\n        ref={ref}\n        {...dialogPreset}\n        // a child dialog on top: this one steps back and up, like a card behind a card\n        animate={childOpen && !reduced ? { opacity: 1, scale: 0.96, y: -8 } : dialogPreset.animate}\n        data-slot=\"dialog-content\"\n        data-size={safeSize}\n        data-scroll-behavior={outside ? \"outside\" : \"inside\"}\n        data-nested={nested || undefined}\n        data-child-open={childOpen || undefined}\n        className={cn(\n          SURFACE,\n          outside ? IN_OVERLAY : safeSize === \"full\" ? FULL : CENTERED,\n          WIDTHS[safeSize],\n          SURFACE_RADII[safeRadius],\n          classNames?.content,\n          className,\n        )}\n        style={{ ...(style as MotionStyle), scaleX: bump, scaleY: bump }}\n      >\n        {inner}\n        {closeButton}\n      </motion.div>\n    </DialogPrimitive.Content>\n  );\n\n  const overlayClass = cn(OVERLAY, nested ? OVERLAY_NESTED : OVERLAY_ROOT, outside && OVERLAY_SCROLL, classNames?.overlay);\n\n  return (\n    <AnimatePresence>\n      {open && (\n        <DialogPrimitive.Portal forceMount container={container}>\n          {outside ? (\n            <DialogPrimitive.Overlay forceMount asChild>\n              <motion.div {...overlayPreset} data-slot=\"dialog-overlay\" className={overlayClass}>\n                {card}\n              </motion.div>\n            </DialogPrimitive.Overlay>\n          ) : (\n            <>\n              {modal && (\n                <DialogPrimitive.Overlay forceMount asChild>\n                  <motion.div {...overlayPreset} data-slot=\"dialog-overlay\" className={overlayClass} />\n                </DialogPrimitive.Overlay>\n              )}\n              {card}\n            </>\n          )}\n        </DialogPrimitive.Portal>\n      )}\n    </AnimatePresence>\n  );\n}\n\n// ---------------------------------------------------------------- header\n\nexport type DialogTitleProps = ComponentProps<typeof DialogPrimitive.Title> & {\n  // keeps the title for screen readers only\n  visuallyHidden?: boolean;\n};\n\nexport function DialogTitle({ visuallyHidden = false, className, ...props }: DialogTitleProps) {\n  const sheet = useContext(DialogContext)?.sheet;\n  const slot = useContext(ContentContext)?.classNames?.title;\n  // the sheet title is the drawer's (17px under the handle)\n  if (sheet)\n    return <DrawerTitle data-slot=\"dialog-title\" visuallyHidden={visuallyHidden} className={cn(slot, className)} {...props} />;\n  const title = <DialogPrimitive.Title data-slot=\"dialog-title\" className={cn(TITLE, slot, className)} {...props} />;\n  return visuallyHidden ? <VisuallyHidden.Root asChild>{title}</VisuallyHidden.Root> : title;\n}\n\nexport type DialogDescriptionProps = ComponentProps<typeof DialogPrimitive.Description>;\n\nexport function DialogDescription({ className, ...props }: DialogDescriptionProps) {\n  const sheet = useContext(DialogContext)?.sheet;\n  const slot = useContext(ContentContext)?.classNames?.description;\n  if (sheet) return <DrawerDescription data-slot=\"dialog-description\" className={cn(slot, className)} {...props} />;\n  return <DialogPrimitive.Description data-slot=\"dialog-description\" className={cn(DESCRIPTION, slot, className)} {...props} />;\n}\n\nexport type DialogIconProps = Omit<\n  ComponentProps<\"span\">,\n  \"color\" | \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\"\n> & {\n  tone?: Tone;\n  // any css color; replaces the tone\n  color?: string;\n};\n\n// a 40px tile in the tone's soft color that pops in as the card lands\nexport function DialogIcon(dialogIconProps: DialogIconProps) {\n  const { tone = \"neutral\", color, className, style, children, ...props } = useDefaults(\"dialog-icon\", dialogIconProps);\n  const content = useContext(ContentContext);\n  const reduced = usePrefersReducedMotion();\n  const safeTone = known(toneClasses, tone, \"neutral\");\n  return (\n    <motion.span\n      data-slot=\"dialog-icon\"\n      data-tone={color ? \"custom\" : safeTone}\n      aria-hidden\n      initial={reduced ? false : { scale: 0.6, opacity: 0 }}\n      animate={{ scale: 1, opacity: 1 }}\n      transition={{ ...springs.bouncy, delay: 0.06 }}\n      className={cn(\n        toneClasses[safeTone],\n        ICON,\n        TILE_RADII[content?.radius ?? \"lg\"],\n        content?.classNames?.icon,\n        className,\n      )}\n      style={{ ...colorVars(color), ...(style as MotionStyle) }}\n      {...props}\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nexport type DialogHeaderProps = Omit<ComponentProps<\"div\">, \"title\" | \"color\"> & {\n  title?: ReactNode;\n  description?: ReactNode;\n  // keeps the title for screen readers only\n  hideTitle?: boolean;\n  // shown in a 40px tile before the title (above it when centered)\n  icon?: ReactNode;\n  // the icon tile's color\n  tone?: Tone;\n  // any css color for the icon tile; replaces the tone\n  color?: string;\n  align?: DialogAlign;\n};\n\nexport function DialogHeader(dialogHeaderProps: DialogHeaderProps) {\n  const {\n    title,\n    description,\n    hideTitle = false,\n    icon,\n    tone = \"neutral\",\n    color,\n    align = \"start\",\n    className,\n    children,\n    ...props\n  } = useDefaults(\"dialog-header\", dialogHeaderProps);\n  const { sheet } = useDialogContext(\"DialogHeader\");\n  const { close, edges, classNames } = useContentContext(\"DialogHeader\");\n  const center = align === \"center\";\n  const hasTitle = filled(title);\n  const hasIcon = filled(icon);\n  const hasDescription = filled(description);\n  const hasChildren = filled(children);\n\n  // children may carry their own DialogTitle; otherwise a dialog always has one, hidden if it has to be\n  const titleNode =\n    hasTitle || !hasChildren ? (\n      <DialogTitle visuallyHidden={hideTitle || !hasTitle}>{hasTitle ? title : FALLBACK_TITLE}</DialogTitle>\n    ) : null;\n  const text = (\n    <>\n      {titleNode}\n      {hasDescription && <DialogDescription>{description}</DialogDescription>}\n      {children}\n    </>\n  );\n\n  // nothing to see: only the title for screen readers, and no empty band at the top\n  if (!hasIcon && !hasDescription && !hasChildren && (hideTitle || !hasTitle)) return titleNode;\n\n  const tile = hasIcon ? (\n    <DialogIcon tone={tone} color={color} className={cn(center && \"mb-2\")}>\n      {icon}\n    </DialogIcon>\n  ) : null;\n  // start with an icon: the tile beside a column of title and description\n  const row = hasIcon && !center;\n  const layout = row ? (\n    <>\n      {tile}\n      <div\n        className={cn(\n          \"flex min-w-0 flex-1 flex-col\",\n          sheet ? \"gap-1\" : \"gap-1.5\",\n          // one visible line (a title or a lone description) centers on the tile\n          (!hasDescription || hideTitle || !hasTitle) && \"self-center\",\n        )}\n      >\n        {text}\n      </div>\n    </>\n  ) : (\n    <>\n      {tile}\n      {text}\n    </>\n  );\n\n  if (sheet) {\n    return (\n      <DrawerHeader\n        data-slot=\"dialog-header\"\n        align={align}\n        className={cn(row && \"flex-row items-start gap-3\", close && (center ? \"px-14\" : \"pe-14\"), classNames?.header, className)}\n        {...props}\n      >\n        {layout}\n      </DrawerHeader>\n    );\n  }\n\n  return (\n    <div\n      data-slot=\"dialog-header\"\n      data-align={align}\n      data-scrolled={edges.top || undefined}\n      className={cn(\n        HEADER,\n        row && \"flex-row items-start gap-4\",\n        center && \"items-center text-center\",\n        close && (center ? \"px-8\" : \"pe-8\"),\n        classNames?.header,\n        className,\n      )}\n      {...props}\n    >\n      {layout}\n    </div>\n  );\n}\n\n// ---------------------------------------------------------------- body and footer\n\nexport type DialogBodyProps = ComponentProps<\"div\">;\n\nexport function DialogBody({ className, style, ref, ...props }: DialogBodyProps) {\n  const { sheet } = useDialogContext(\"DialogBody\");\n  const { outside, setEdges, classNames } = useContentContext(\"DialogBody\");\n  const { ref: edgesRef, edges } = useScrollEdges<HTMLDivElement>({ fade: 16, disabled: sheet || outside });\n\n  // the header and footer draw their hairlines from these\n  useEffect(() => {\n    setEdges(edges.top, edges.bottom);\n  }, [edges.top, edges.bottom, setEdges]);\n  useEffect(() => () => setEdges(false, false), [setEdges]);\n\n  const node = useRef<HTMLDivElement>(null);\n  const mergedRef = useMergedRef(useMergedRef(edgesRef, node), ref);\n\n  // an overflowing body with nothing focusable inside (terms, a long text) takes focus itself, so the keyboard can\n  // scroll it; one with fields keeps them first in line\n  const [focusable, setFocusable] = useState(false);\n  const scrolls = edges.top || edges.bottom;\n  useEffect(() => {\n    setFocusable(scrolls && !!node.current && !node.current.querySelector(TABBABLE));\n  }, [scrolls]);\n\n  // the sheet body scrolls first and only then lets the sheet drag\n  if (sheet)\n    return (\n      <DrawerBody ref={ref} data-slot=\"dialog-body\" className={cn(SHEET_BODY, classNames?.body, className)} style={style} {...props} />\n    );\n\n  return (\n    <div\n      ref={mergedRef}\n      data-slot=\"dialog-body\"\n      tabIndex={focusable && !outside ? 0 : undefined}\n      className={cn(outside ? BODY_OUTSIDE : BODY, classNames?.body, className)}\n      // text scrolling under the hairlines fades out instead of being cut\n      style={outside ? style : { ...fadeMaskStyle(\"y\"), ...style }}\n      {...props}\n    />\n  );\n}\n\nexport type DialogFooterProps = ComponentProps<\"div\">;\n\nexport function DialogFooter({ className, ...props }: DialogFooterProps) {\n  const { sheet } = useDialogContext(\"DialogFooter\");\n  const { edges, classNames } = useContentContext(\"DialogFooter\");\n  if (sheet)\n    return <DrawerFooter data-slot=\"dialog-footer\" className={cn(SHEET_FOOTER, classNames?.footer, className)} {...props} />;\n  return (\n    <div\n      data-slot=\"dialog-footer\"\n      data-overflow={edges.bottom || undefined}\n      className={cn(FOOTER, classNames?.footer, className)}\n      {...props}\n    />\n  );\n}\n\n// ---------------------------------------------------------------- simple api\n\nexport type DialogProps = Omit<DialogRootProps, \"children\"> &\n  Omit<DialogContentProps, \"children\" | \"title\" | \"color\"> & {\n    // the element that opens it (rendered with asChild, so a single element that takes a ref)\n    trigger?: ReactNode;\n    title?: ReactNode;\n    description?: ReactNode;\n    // keeps the title for screen readers only\n    hideTitle?: boolean;\n    // a 40px tile before the title\n    icon?: ReactNode;\n    // the icon tile's color\n    tone?: Tone;\n    // any css color for the icon tile; replaces the tone\n    color?: string;\n    // start reads like a form, center like an announcement (icon above the title)\n    headerAlign?: DialogAlign;\n    // the body; it scrolls between the pinned header and footer\n    children?: ReactNode;\n    // actions under the body, primary last: on the right on desktop, on top when stacked\n    footer?: ReactNode;\n  };\n\nexport function Dialog(dialogProps: DialogProps) {\n  const {\n    open,\n    defaultOpen = false,\n    onOpenChange,\n    dismissible = true,\n    responsive = true,\n    modal = true,\n    shortcut,\n    container,\n    size = \"md\",\n    radius = \"lg\",\n    scrollBehavior = \"inside\",\n    showClose = true,\n    closeLabel = \"Close\",\n    classNames,\n    trigger,\n    title,\n    description,\n    hideTitle = false,\n    icon,\n    tone = \"neutral\",\n    color,\n    headerAlign = \"start\",\n    footer,\n    children,\n    ...props\n  } = useDefaults(\"dialog\", dialogProps);\n  const hasDescription = filled(description);\n  return (\n    <DialogRoot\n      open={open}\n      defaultOpen={defaultOpen}\n      onOpenChange={onOpenChange}\n      dismissible={dismissible}\n      responsive={responsive}\n      modal={modal}\n      shortcut={shortcut}\n      container={container}\n    >\n      {trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}\n      <DialogContent\n        size={size}\n        radius={radius}\n        scrollBehavior={scrollBehavior}\n        showClose={showClose}\n        closeLabel={closeLabel}\n        classNames={classNames}\n        // radix's documented opt-out when there is nothing to describe the dialog\n        {...(hasDescription ? {} : { \"aria-describedby\": undefined })}\n        {...props}\n      >\n        <DialogHeader\n          title={title}\n          description={description}\n          hideTitle={hideTitle}\n          icon={icon}\n          tone={tone}\n          color={color}\n          align={headerAlign}\n        />\n        {filled(children) ? <DialogBody>{children}</DialogBody> : null}\n        {filled(footer) ? (\n          <DialogFooter className={cn(headerAlign === \"center\" && \"sm:justify-center\")}>{footer}</DialogFooter>\n        ) : null}\n      </DialogContent>\n    </DialogRoot>\n  );\n}\n\n"}],"categories":["overlays"],"meta":{"kind":"component","category":"overlays","tags":["dialog","modal","popup","overlay","form","bottom sheet","responsive","nested","confirm","lightbox","shortcut","radix"],"exportName":"Dialog","defaultExport":false,"access":"free","version":"1.0.0","hash":"sha256-7afac5c35bc81596fbf9","usage":"import { Pencil } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Dialog, DialogClose } from \"@/components/ui/dialog\"\n\nexport function EditListing({ saving, onSave }: { saving: boolean; onSave: () => void }) {\n  return (\n    <Dialog\n      trigger={\n        <Button variant=\"soft\" leftIcon={<Pencil />}>\n          Edit\n        </Button>\n      }\n      title=\"Edit listing\"\n      description=\"Changes show up for buyers right away.\"\n      icon={<Pencil />}\n      closeLabel=\"Close dialog\"\n      shortcut=\"mod+e\"\n      footer={\n        <>\n          <DialogClose asChild>\n            <Button variant=\"soft\">Cancel</Button>\n          </DialogClose>\n          <Button loading={saving} loadingText=\"Saving\" onClick={onSave}>\n            Save\n          </Button>\n        </>\n      }\n    >\n      <p className=\"text-sm text-muted-foreground\">Title, price, city and description</p>\n    </Dialog>\n  )\n}"}}