{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dialog",
  "type": "registry:ui",
  "title": "Dialog",
  "description": "An accessible React dialog for Next.js and Tailwind CSS: a modal window that grows out of its trigger, keeps focus inside and returns it, built on Radix Dialog.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "lucide-react@^1.52.0",
    "motion@^14.0.0",
    "radix-ui@^1.7.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json",
    "https://ui.fewtone.com/r/motion.json",
    "https://ui.fewtone.com/r/button.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/dialog.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { blur, duration, ease, spring } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { XIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport {\n  type ComponentProps,\n  createContext,\n  type Ref,\n  type RefObject,\n  useCallback,\n  useContext,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\n/*\n * Dialog, built on Radix Dialog: focus moves into the window and stays there, Escape and the backdrop close\n * it, focus goes back to the trigger, and the page behind is hidden from screen readers and doesn't scroll.\n * - The root keeps the open state, controlled (`open`) or not (`defaultOpen`), so the window can play its\n *   exit: the portal stays mounted (forceMount) inside AnimatePresence until the exit ends.\n * - Motion (.agents/rules/motion.md): the window comes through the blur, scaling up from 96% on\n *   spring.snappy, and leaves on a timed duration.base + ease.in. The backdrop only fades. With reduced\n *   motion, both only fade.\n * - It grows out of what opened it: the scale's origin is the trigger's centre, so the window seems to come\n *   from that button and shrinks back into it as it closes.\n * - While it leaves, clicks go through to the page, so the page is usable again at once. Focus then goes\n *   back to the trigger, unless the person has already focused something else there.\n * - Alert Dialog shares the window, its motion and the open state from this file (bottom).\n */\n\n/** What every part needs: the open state, and the trigger the window grows from */\nexport type DialogState = { open: boolean; trigger: RefObject<HTMLElement | null> };\n\nconst DialogContext = createContext<DialogState | null>(null);\n\nfunction useDialog(part: string) {\n  const dialog = useContext(DialogContext);\n  if (!dialog) throw new Error(`${part} must be used inside Dialog`);\n  return dialog;\n}\n\n/** The open state, controlled (`open`) or not (`defaultOpen`), and a ref for the trigger */\nfunction useDialogState({\n  open,\n  defaultOpen,\n  onOpenChange,\n}: {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}) {\n  const [own, setOwn] = useState(defaultOpen ?? false);\n  const trigger = useRef<HTMLElement | null>(null);\n  const current = open ?? own;\n  const setOpen = useCallback(\n    (next: boolean) => {\n      setOwn(next);\n      onOpenChange?.(next);\n    },\n    [onOpenChange],\n  );\n  const state = useMemo<DialogState>(() => ({ open: current, trigger }), [current]);\n  return { state, setOpen };\n}\n\n/** Hands a node to our ref and to the caller's */\nfunction useMergedRef<T>(own: RefObject<T | null>, theirs?: Ref<T>) {\n  return useCallback(\n    (node: T | null) => {\n      own.current = node;\n      if (typeof theirs === \"function\") theirs(node);\n      else if (theirs) theirs.current = node;\n    },\n    [own, theirs],\n  );\n}\n\n/** Leaving is timed rather than a spring, so it's over quickly */\nconst leave = { duration: duration.base, ease: ease.in };\n\n/** The backdrop only fades: a full-screen blur would cost too much (ADR 0011) */\nconst dialogOverlayMotion = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1, transition: { duration: duration.base, ease: ease.out } },\n  exit: { opacity: 0, transition: leave },\n};\n\n/** The look shared with Alert Dialog */\nconst dialogStyles = {\n  overlay: \"fixed inset-0 z-(--z-overlay) bg-overlay\",\n  // Centres the window and keeps it 16px inside the screen; clicks pass through it to the backdrop\n  positioner: \"pointer-events-none fixed inset-0 z-(--z-modal) flex items-center justify-center p-4\",\n  window: [\n    \"pointer-events-auto relative grid max-h-full w-full max-w-lg gap-4 overflow-y-auto rounded-lg border border-border bg-background p-6 text-foreground shadow-lg outline-hidden\",\n    // Leaving, it no longer catches clicks\n    \"data-[state=closed]:pointer-events-none\",\n  ],\n  header: \"grid gap-1.5\",\n  footer: \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n  title: \"text-base font-semibold\",\n  description: \"text-sm text-muted-foreground\",\n};\n\n/**\n * For Radix's onCloseAutoFocus, which runs once the window is gone: focus goes back to the trigger, unless\n * the person has already moved on. Clicks reach the page while the window leaves, so they may have focused\n * something there; otherwise the focused element went with the window and focus is on the body.\n */\nfunction keepMovedFocus(event: Event) {\n  if (document.activeElement !== document.body) event.preventDefault();\n}\n\n/**\n * The window's motion, as props for a motion.div (shared with Alert Dialog). It comes through the blur,\n * scaling up from 96% on spring.snappy, and leaves on a timed duration.base + ease.in, growing from the\n * centre of what opened it.\n */\nfunction useDialogWindow(trigger: RefObject<HTMLElement | null>) {\n  const reduce = useReducedMotion() ?? false;\n  const hidden = { opacity: 0, filter: reduce ? blur.shown : blur.hidden, scale: reduce ? 1 : 0.96 };\n\n  // Measured once, as the window mounts and before Radix moves focus into it. What opened it is the focused\n  // element (a key press, or a click in most browsers), else the trigger (Safari doesn't focus buttons on\n  // click). Opened from code with nothing focused, it grows from its own centre.\n  const ref = useCallback(\n    (node: HTMLElement | null) => {\n      if (!node) return;\n      const focused = document.activeElement;\n      const from =\n        focused instanceof HTMLElement && focused !== document.body && !node.contains(focused)\n          ? focused\n          : trigger.current;\n      const frame = node.offsetParent?.getBoundingClientRect();\n      const box = from?.getBoundingClientRect();\n      if (!frame || !box || (box.width === 0 && box.height === 0)) return;\n      // The origin is in the window's own box before any transform: its offset in the positioner, not its\n      // scaled on-screen rect\n      const x = box.left + box.width / 2 - frame.left - node.offsetLeft;\n      const y = box.top + box.height / 2 - frame.top - node.offsetTop;\n      node.style.transformOrigin = `${x}px ${y}px`;\n    },\n    [trigger],\n  );\n\n  return {\n    ref,\n    initial: hidden,\n    animate: { opacity: 1, filter: blur.shown, scale: 1 },\n    exit: { ...hidden, transition: leave },\n    // The fade and the blur are eased; the scale springs\n    transition: { duration: duration.base, ease: ease.out, scale: spring.snappy },\n  };\n}\n\nfunction Dialog({ open, defaultOpen, onOpenChange, ...props }: ComponentProps<typeof DialogPrimitive.Root>) {\n  const { state, setOpen } = useDialogState({ open, defaultOpen, onOpenChange });\n  return (\n    <DialogContext.Provider value={state}>\n      <DialogPrimitive.Root data-slot=\"dialog\" open={state.open} onOpenChange={setOpen} {...props} />\n    </DialogContext.Provider>\n  );\n}\n\nfunction DialogTrigger({ ref, ...props }: ComponentProps<typeof DialogPrimitive.Trigger>) {\n  const dialog = useDialog(\"DialogTrigger\");\n  const setRef = useMergedRef(dialog.trigger, ref);\n  return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" ref={setRef} {...props} />;\n}\n\n/** Stays mounted while the dialog is open and while its exit plays */\nfunction DialogPortal({\n  children,\n  ...props\n}: Omit<ComponentProps<typeof DialogPrimitive.Portal>, \"forceMount\">) {\n  const { open } = useDialog(\"DialogPortal\");\n  return (\n    // initial={false}: a dialog that starts open is simply there, with no entrance\n    <AnimatePresence initial={false}>\n      {open ? (\n        <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} forceMount>\n          {children}\n        </DialogPrimitive.Portal>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n\nfunction DialogOverlay({ className, style, ...props }: ComponentProps<typeof DialogPrimitive.Overlay>) {\n  const { open } = useDialog(\"DialogOverlay\");\n  return (\n    <DialogPrimitive.Overlay\n      data-slot=\"dialog-overlay\"\n      className={cn(dialogStyles.overlay, className)}\n      // Radix keeps the backdrop clickable; while it fades out, clicks go through to the page\n      style={{ pointerEvents: open ? \"auto\" : \"none\", ...style }}\n      {...props}\n      asChild\n      forceMount\n    >\n      <motion.div {...dialogOverlayMotion} />\n    </DialogPrimitive.Overlay>\n  );\n}\n\nexport type DialogContentProps = ComponentProps<typeof DialogPrimitive.Content> & {\n  /** Show a close button in the top corner */\n  showCloseButton?: boolean;\n};\n\nfunction DialogContent({\n  className,\n  children,\n  showCloseButton = true,\n  onCloseAutoFocus,\n  ...props\n}: DialogContentProps) {\n  const dialog = useDialog(\"DialogContent\");\n  const windowMotion = useDialogWindow(dialog.trigger);\n  return (\n    <DialogPortal>\n      <DialogOverlay />\n      <div data-slot=\"dialog-positioner\" className={dialogStyles.positioner}>\n        <DialogPrimitive.Content\n          data-slot=\"dialog-content\"\n          className={cn(\n            dialogStyles.window,\n            // The header stops short of the close button, so a long title never runs under it\n            showCloseButton && \"**:data-[slot=dialog-header]:pe-8\",\n            className,\n          )}\n          onCloseAutoFocus={(event) => {\n            onCloseAutoFocus?.(event);\n            keepMovedFocus(event);\n          }}\n          {...props}\n          asChild\n          forceMount\n        >\n          <motion.div {...windowMotion}>\n            {children}\n            {showCloseButton ? (\n              <DialogPrimitive.Close asChild>\n                {/* Centred on the title's line, its icon lined up with the window's padding */}\n                <Button\n                  data-slot=\"dialog-close\"\n                  variant=\"ghost\"\n                  size=\"icon-sm\"\n                  aria-label=\"Close\"\n                  className=\"absolute end-4 top-5 text-muted-foreground hover:text-foreground\"\n                >\n                  <XIcon />\n                </Button>\n              </DialogPrimitive.Close>\n            ) : null}\n          </motion.div>\n        </DialogPrimitive.Content>\n      </div>\n    </DialogPortal>\n  );\n}\n\nfunction DialogHeader({ className, ...props }: ComponentProps<\"div\">) {\n  return <div data-slot=\"dialog-header\" className={cn(dialogStyles.header, className)} {...props} />;\n}\n\n/** Actions, stacked on phones with the main one on top, in a row at the end from the sm breakpoint */\nfunction DialogFooter({ className, ...props }: ComponentProps<\"div\">) {\n  return <div data-slot=\"dialog-footer\" className={cn(dialogStyles.footer, className)} {...props} />;\n}\n\nfunction DialogTitle({ className, ...props }: ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"dialog-title\"\n      className={cn(dialogStyles.title, className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogDescription({ className, ...props }: ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"dialog-description\"\n      className={cn(dialogStyles.description, className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogClose(props: ComponentProps<typeof DialogPrimitive.Close>) {\n  return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n  // Shared with Alert Dialog, so the two look and move the same\n  dialogOverlayMotion,\n  dialogStyles,\n  keepMovedFocus,\n  useDialogState,\n  useDialogWindow,\n  useMergedRef,\n};\n"
    }
  ],
  "categories": [
    "overlays"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/dialog"
}
