{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dialog",
  "title": "Dialog",
  "description": "Dialog from the Get IT Done design system.",
  "dependencies": [
    "@radix-ui/react-dialog@^1.3.1",
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/nested-layer.json",
    "https://zero-design.itdone.de/r/return-focus.json",
    "https://zero-design.itdone.de/r/sticky-inset.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/dialog.tsx",
      "content": "'use client';\n\nimport { OpenerCapture, useReturnFocusFallback } from '@/lib/return-focus';\nimport * as DialogPrimitive from '@radix-ui/react-dialog';\nimport { X } from 'lucide-react';\nimport * as React from 'react';\n\nimport { EscapeGuardContext, guardNestedEscape, useGuardedOpen } from '@/lib/nested-layer';\nimport { INSET_SCOPE_ATTRIBUTE, scrollInsetClasses, useStickyInset } from '@/lib/sticky-inset';\nimport { cn } from '@/lib/utils';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\n\nconst Dialog = ({ open, defaultOpen, onOpenChange, ...props }: React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root>) => {\n  const state = useGuardedOpen({ open, defaultOpen, onOpenChange });\n  return (\n    <EscapeGuardContext.Provider value={state.guard}>\n      <DialogPrimitive.Root open={state.open} onOpenChange={state.onOpenChange} {...props} />\n    </EscapeGuardContext.Provider>\n  );\n};\nDialog.displayName = 'Dialog';\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\n// Native fields plus the ARIA form controls that Radix renders as buttons (radio, checkbox, switch) and other value widgets.\nconst FIELD_SELECTOR = [\n  'input:not([type=\"hidden\"]):not([disabled])',\n  'select:not([disabled])',\n  'textarea:not([disabled])',\n  '[contenteditable=\"true\"]',\n  '[role=\"textbox\"]',\n  '[role=\"combobox\"]',\n  '[role=\"searchbox\"]',\n  '[role=\"spinbutton\"]',\n  '[role=\"slider\"]',\n  'button[role=\"radio\"]:not([disabled])',\n  'button[role=\"checkbox\"]:not([disabled])',\n  'button[role=\"switch\"]:not([disabled])',\n].join(', ');\nconst FOCUSABLE_SELECTOR = `${FIELD_SELECTOR}, a[href], button:not([disabled]), summary, [tabindex]:not([tabindex=\"-1\"]):not([data-slot=\"dialog-body\"])`;\n\nconst isUsable = (el: HTMLElement) => el.getAttribute('aria-disabled') !== 'true' && !el.closest('[hidden], [aria-hidden=\"true\"], [inert]');\n\n// Radio groups have a single tab stop: the checked item, else the first. Skip the others so focus lands where Tab would.\nconst isRadioTabStop = (el: HTMLElement, root: ParentNode) => {\n  if (el instanceof HTMLInputElement && el.type === 'radio') {\n    if (el.checked || !el.name) return true;\n    const group = Array.from(root.querySelectorAll<HTMLInputElement>('input[type=\"radio\"]')).filter((radio) => radio.name === el.name && radio.form === el.form);\n    return !group.some((radio) => radio.checked);\n  }\n  if (el.getAttribute('role') === 'radio') {\n    // Radix keeps every item at tabindex -1 until focus has entered the group, so fall back to the first item.\n    const group = el.closest('[role=\"radiogroup\"]') ?? root;\n    const items = Array.from(group.querySelectorAll<HTMLElement>('[role=\"radio\"]')).filter((item) => !item.hasAttribute('disabled') && isUsable(item));\n    const stop = items.find((item) => item.getAttribute('aria-checked') === 'true') ?? items.find((item) => item.getAttribute('tabindex') !== '-1') ?? items[0];\n    return el === stop;\n  }\n  return true;\n};\n\n// First form control in DOM order inside `scope`. It wins over tabs, links and buttons, even when those come earlier.\nconst firstControl = (scope: HTMLElement) =>\n  Array.from(scope.querySelectorAll<HTMLElement>(FIELD_SELECTOR)).find((el) => isUsable(el) && isRadioTabStop(el, scope)) ?? null;\n\n// Initial focus for a dialog: the scrolling body never takes it. Priority: first form control in the body, then in the rest of the panel, then the first other focusable, then the panel.\n// Without a body this only applies when `firstField` is set, otherwise Radix keeps its default (first tabbable).\nconst focusInitial = (event: Event, firstField: boolean) => {\n  const content = event.target as HTMLElement | null;\n  const body = content?.querySelector<HTMLElement>('[data-slot=\"dialog-body\"]') ?? null;\n  if (!content || (!body && !firstField)) return;\n  const control = (body ? firstControl(body) : null) ?? firstControl(content);\n  if (!body && !control) return;\n  event.preventDefault();\n  const target = control ?? Array.from(content.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).find((el) => el !== body) ?? content;\n  target.focus();\n};\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => {\n  const zero = usePreset() === 'zero';\n  return (\n    <DialogPrimitive.Overlay\n      ref={ref}\n      className={cn(\n        'fixed inset-0 z-dialog data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n        zero ? 'bg-overlay-scrim' : 'bg-overlay',\n        className,\n      )}\n      {...props}\n    />\n  );\n});\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\n// Panel widths from the sheet size tokens: sm 440, md 520, lg 640, xl 900. Omit `size` for the original max-w-lg panel.\nconst dialogSizeWidth = {\n  sm: 'max-w-sheet-sm',\n  md: 'max-w-sheet-md',\n  lg: 'max-w-sheet-lg',\n  xl: 'max-w-sheet-xl',\n} as const;\n\ninterface DialogContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {\n  size?: keyof typeof dialogSizeWidth;\n  // Long content: the panel stops scrolling as a whole. Put the content in DialogBody (scrolls) and keep DialogHeader and DialogFooter outside it (they stay in view).\n  scrollable?: boolean;\n  // Hides the corner close button, for confirm dialogs whose footer already closes them.\n  hideClose?: boolean;\n  // Accessible name of the close button. Wins over the provider text.\n  closeLabel?: string;\n  // `first-field` moves initial focus to the first form control (input, select, textarea, radio, checkbox, switch) in DOM order, before tabs, links and buttons, also in dialogs without DialogBody. Default `auto` keeps the Radix default for those dialogs. A `scrollable` DialogBody always uses this priority.\n  initialFocus?: 'auto' | 'first-field';\n}\n\nconst DialogContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Content>, DialogContentProps>(\n  ({ className, children, size: sizeProp, scrollable = false, hideClose = false, closeLabel, initialFocus = 'auto', onOpenAutoFocus, onEscapeKeyDown, onCloseAutoFocus, ...props }, ref) => {\n  const returnFocus = useReturnFocusFallback(onCloseAutoFocus);\n  const l = useLabels({ close: closeLabel });\n  const zero = usePreset() === 'zero';\n  const escapeGuard = React.useContext(EscapeGuardContext);\n  // Zero: confirm width (440) unless a size is given.\n  const size = sizeProp ?? (zero ? 'sm' : undefined);\n  return (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      onOpenAutoFocus={(event) => {\n        onOpenAutoFocus?.(event);\n        if (!event.defaultPrevented) focusInitial(event, initialFocus === 'first-field');\n      }}\n      onCloseAutoFocus={returnFocus.onCloseAutoFocus}\n      onEscapeKeyDown={(event) => {\n        onEscapeKeyDown?.(event);\n        // Escape inside a select list, menu or popover closes that layer only.\n        guardNestedEscape(escapeGuard, event);\n      }}\n      {...{ [INSET_SCOPE_ATTRIBUTE]: '' }}\n      className={cn(\n        'max-h-screen',\n        scrollInsetClasses,\n        scrollable ? 'overflow-hidden' : 'overflow-y-auto',\n        'fixed',\n        'left-[50%]',\n        'top-[50%]',\n        'z-dialog',\n        scrollable ? 'flex flex-col' : 'grid',\n        'w-full',\n        !size && 'max-w-lg',\n        'translate-x-[-50%]',\n        'translate-y-[-50%]',\n        zero ? 'gap-3' : 'gap-4',\n        'border border-solid',\n        zero ? 'border-border-default' : 'border-hairline',\n        'bg-background',\n        'p-6',\n        zero ? 'shadow-elevation-raised' : 'shadow-dialog',\n        'duration-200',\n        'data-[state=open]:animate-in',\n        'data-[state=closed]:animate-out',\n        'data-[state=closed]:fade-out-0',\n        'data-[state=open]:fade-in-0',\n        'data-[state=closed]:zoom-out-95',\n        'data-[state=open]:zoom-in-[0.98]',\n        'data-[state=closed]:slide-out-to-left-1/2',\n        'data-[state=closed]:slide-out-to-top-[48%]',\n        'data-[state=open]:slide-in-from-left-1/2',\n        'data-[state=open]:slide-in-from-top-[48%]',\n        'sm:rounded-dialog',\n        size && dialogSizeWidth[size],\n        className,\n      )}\n      {...props}\n    >\n      <OpenerCapture onCapture={returnFocus.remember} />\n      {children}\n      {hideClose ? null : (\n        <DialogPrimitive.Close data-slot=\"dialog-close\" className={cn('absolute right-3 top-3 inline-flex items-center justify-center rounded-control text-icon-subtle transition-colors hover:bg-muted hover:text-icon-default active:bg-muted-strong focus:outline-none focus-visible:ring-focus focus-visible:ring-offset-focus focus-visible:ring-offset-background focus-visible:ring-ring-default disabled:pointer-events-none data-[state=open]:bg-secondary',\n          // Same fixed 40px square as the Sheet close, independent of theme and preset.\n          'h-10 w-10',\n        )}>\n          <X className=\"h-icon-md w-icon-md\" aria-hidden=\"true\" />\n          <span className=\"sr-only\">{l.close}</span>\n        </DialogPrimitive.Close>\n      )}\n    </DialogPrimitive.Content>\n  </DialogPortal>\n  );\n  },\n);\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\ninterface DialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** `horizontal` puts a DialogFeaturedIcon beside the text column. Default `vertical` is unchanged. */\n  orientation?: 'vertical' | 'horizontal';\n}\n\nconst DialogHeader = React.forwardRef<HTMLDivElement, DialogHeaderProps>(({ className, orientation = 'vertical', ...props }, ref) => (\n  <div\n    ref={useStickyInset<HTMLDivElement>('top', ref)}\n    className={cn(\n      'flex text-left',\n      orientation === 'horizontal' ? 'flex-row items-start gap-4 pr-8' : 'flex-col space-y-[6px]',\n      className,\n    )}\n    {...props}\n  />\n));\nDialogHeader.displayName = 'DialogHeader';\n\nconst DialogFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (\n  <div ref={useStickyInset<HTMLDivElement>('bottom', ref)} className={cn('flex shrink-0 flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)} {...props} />\n));\nDialogFooter.displayName = 'DialogFooter';\n\n// Scrolling middle part of a `scrollable` DialogContent. The overflow clips at the padding box, so the body pads itself by 4px and pulls back with a negative margin: layout stays put and focus rings of its controls are not cut off. It never takes initial focus (DialogContent focuses the first field). It is a tab stop only while it holds nothing focusable, so keyboard users can still scroll plain content.\nconst DialogBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, forwardedRef) => {\n  const innerRef = React.useRef<HTMLDivElement | null>(null);\n  const [hasFocusableChild, setHasFocusableChild] = React.useState(false);\n  const setRef = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      innerRef.current = node;\n      if (typeof forwardedRef === 'function') forwardedRef(node);\n      else if (forwardedRef) (forwardedRef as React.MutableRefObject<HTMLDivElement | null>).current = node;\n    },\n    [forwardedRef],\n  );\n\n  React.useLayoutEffect(() => {\n    const node = innerRef.current;\n    if (!node) return undefined;\n    const update = () => setHasFocusableChild(node.querySelector(FOCUSABLE_SELECTOR) !== null);\n    update();\n    const observer = new MutationObserver(update);\n    observer.observe(node, { childList: true, subtree: true, attributes: true, attributeFilter: ['disabled', 'tabindex', 'href'] });\n    return () => observer.disconnect();\n  }, []);\n\n  return (\n    <div\n      ref={setRef}\n      // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n      tabIndex={hasFocusableChild ? undefined : 0}\n      data-slot=\"dialog-body\"\n      className={cn('-m-1 min-h-0 flex-1 overflow-y-auto p-1', scrollInsetClasses, 'focus-visible:outline-none focus-visible:ring-focus focus-visible:ring-offset-focus focus-visible:ring-offset-background focus-visible:ring-ring-default', className)}\n      {...props}\n    />\n  );\n});\nDialogBody.displayName = 'DialogBody';\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => {\n  const zero = usePreset() === 'zero';\n  return (\n    <DialogPrimitive.Title\n      ref={ref}\n      className={cn(zero ? 'typography-label-highlight' : 'text-base font-semibold leading-none tracking-[-0.01em]', className)}\n      {...props}\n    />\n  );\n});\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => {\n  const zero = usePreset() === 'zero';\n  return (\n    <DialogPrimitive.Description\n      ref={ref}\n      className={cn(zero ? 'typography-ui text-text-subtle' : 'typography-p-ui text-sm text-muted-foreground', className)}\n      {...props}\n    />\n  );\n});\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nconst dialogIconTones = {\n  neutral: 'bg-muted text-icon-default',\n  brand: 'bg-general-brand-container text-general-brand-on-container',\n  success: 'bg-general-success-container text-general-success-on-container',\n  warning: 'bg-general-warning-container text-general-warning-on-container',\n  destructive: 'bg-general-destructive-container text-general-destructive-on-container',\n  info: 'bg-general-info-container text-general-info-on-container',\n} as const;\n\ninterface DialogFeaturedIconProps extends React.HTMLAttributes<HTMLSpanElement> {\n  tone?: keyof typeof dialogIconTones;\n}\n\n// Round tinted icon tile for the modal header (Figma \"_Modal header\" featured icon). Decorative.\nconst DialogFeaturedIcon = ({ className, tone = 'neutral', ...props }: DialogFeaturedIconProps) => (\n  <span\n    aria-hidden\n    className={cn(\n      'grid h-control-xl w-control-xl shrink-0 place-items-center rounded-full [&>svg]:h-icon-lg [&>svg]:w-icon-lg',\n      dialogIconTones[tone],\n      className,\n    )}\n    {...props}\n  />\n);\nDialogFeaturedIcon.displayName = 'DialogFeaturedIcon';\n\nexport {\n  type DialogContentProps,\n  type DialogFeaturedIconProps,\n  type DialogHeaderProps,\n  DialogFeaturedIcon,\n  Dialog,\n  DialogBody,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}