{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dialog","title":"Dialog","description":"Base UI-backed modal task boundary.","dependencies":["@base-ui/react@1.7.0","lucide-react@1.18.0"],"registryDependencies":["@siteplane/base","@siteplane/button","@siteplane/scroll-area"],"files":[{"path":"packages/ui/src/components/dialog/dialog.tsx","content":"\"use client\";\n\nimport { Dialog as DialogPrimitive } from \"@base-ui/react/dialog\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { XIcon } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { SiteplaneUIPortalScope } from \"../../system/provider/siteplane-ui-provider\";\nimport { surfaceClassNames } from \"../../system/styles/surface\";\nimport { cn, type WithStringClassName } from \"../../system/utils/cn\";\nimport { baseClass, slotClass } from \"../../system/utils/slot-class\";\nimport { KitButton } from \"../button/button-internal\";\nimport { ScrollArea } from \"../scroll-area\";\nimport type { KitDialogPopupProps } from \"./dialog-internal\";\n\nimport \"./dialog.css\";\n\nexport const DialogCreateHandle: typeof DialogPrimitive.createHandle =\n  DialogPrimitive.createHandle;\nexport const Dialog: typeof DialogPrimitive.Root = DialogPrimitive.Root;\n// Base UI's own Portal type, with className narrowed to a string.\nexport const DialogPortal: React.ForwardRefExoticComponent<\n  WithStringClassName<Omit<DialogPrimitive.Portal.Props, \"ref\">> &\n    React.RefAttributes<HTMLDivElement>\n> = DialogPrimitive.Portal;\n\nexport function DialogTrigger(\n  props: WithStringClassName<DialogPrimitive.Trigger.Props>,\n): React.ReactElement {\n  return (\n    <DialogPrimitive.Trigger\n      data-slot=\"dialog-trigger\"\n      {...props}\n      className={cn(slotClass(\"dialog-trigger\", props), props.className)}\n    />\n  );\n}\n\nexport function DialogClose(\n  props: WithStringClassName<DialogPrimitive.Close.Props>,\n): React.ReactElement {\n  return (\n    <DialogPrimitive.Close\n      data-slot=\"dialog-close\"\n      {...props}\n      className={cn(slotClass(\"dialog-close\", props), props.className)}\n    />\n  );\n}\n\nexport function DialogBackdrop({\n  className,\n  ...props\n}: WithStringClassName<DialogPrimitive.Backdrop.Props>): React.ReactElement {\n  return (\n    <DialogPrimitive.Backdrop\n      className={cn(\n        slotClass(\"dialog-backdrop\", props),\n        surfaceClassNames.overlayBackdrop,\n        className,\n      )}\n      data-slot=\"dialog-backdrop\"\n      {...props}\n    />\n  );\n}\n\nexport function DialogViewport({\n  className,\n  ...props\n}: WithStringClassName<DialogPrimitive.Viewport.Props>): React.ReactElement {\n  return (\n    <DialogPrimitive.Viewport\n      className={cn(\n        slotClass(\"dialog-viewport\", props),\n        surfaceClassNames.dialogOverlayViewport,\n        className,\n      )}\n      data-slot=\"dialog-viewport\"\n      {...props}\n    />\n  );\n}\n\n/**\n * The width of a Dialog or AlertDialog popup on the modal scale: `xs` 400 px,\n * `sm` 440 px (the default), `md` 600 px and `lg` 880 px. It applies from the\n * `sm` breakpoint up; a smaller screen shows the full-width bottom sheet at\n * every size.\n */\nexport type DialogSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\ntype DialogPopupProps = WithStringClassName<DialogPrimitive.Popup.Props> & {\n  showCloseButton?: boolean;\n  /**\n   * The popup width: `xs` 400 px, `sm` 440 px (the default), `md` 600 px or\n   * `lg` 880 px, from the `sm` breakpoint up. A `max-w-*` class in\n   * `className` still wins.\n   */\n  size?: DialogSize;\n  closeProps?: WithStringClassName<DialogPrimitive.Close.Props>;\n  portalProps?: WithStringClassName<DialogPrimitive.Portal.Props>;\n};\n\n// The elements a browser can focus with Tab, as Base UI's focus manager finds\n// them: the candidates that are enabled, rendered, outside an inert or hidden\n// subtree and not taken out of the tab order.\nconst tabbableCandidates =\n  'a[href],button,input,select,textarea,summary,details,iframe,object,embed,[tabindex],[contenteditable]:not([contenteditable=\"false\"]),audio[controls],video[controls]';\n\nfunction firstTabbable(popup: HTMLElement): HTMLElement | null {\n  for (const element of popup.querySelectorAll<HTMLElement>(\n    tabbableCandidates,\n  )) {\n    if (\n      element.tabIndex < 0 ||\n      element.matches(\":disabled\") ||\n      (element instanceof HTMLInputElement && element.type === \"hidden\") ||\n      element.closest(\"[inert], [hidden]\") ||\n      element.getClientRects().length === 0 ||\n      getComputedStyle(element).visibility === \"hidden\"\n    )\n      continue;\n    return element;\n  }\n  return null;\n}\n\n/** Whether every scrolling box between the element and the popup shows it whole. */\nfunction startsInView(element: HTMLElement, popup: HTMLElement): boolean {\n  const box = element.getBoundingClientRect();\n  for (\n    let parent = element.parentElement;\n    parent && popup.contains(parent);\n    parent = parent.parentElement\n  ) {\n    if (\n      parent.scrollHeight <= parent.clientHeight &&\n      parent.scrollWidth <= parent.clientWidth\n    )\n      continue;\n    // Content that overflows a box which does not clip it stays in view.\n    const { overflowX, overflowY } = getComputedStyle(parent);\n    if (overflowX === \"visible\" && overflowY === \"visible\") continue;\n    const area = parent.getBoundingClientRect();\n    if (\n      box.top < area.top ||\n      box.bottom > area.bottom ||\n      box.left < area.left ||\n      box.right > area.right\n    )\n      return false;\n  }\n  return true;\n}\n\n/**\n * Where focus goes when the dialog opens. Base UI focuses the first tabbable\n * element and scrolls it into view, so a dialog whose content scrolls opened\n * at its end when that element sat below the fold, such as a field under long\n * terms (stress finding S-14). When the first tabbable element starts out of\n * view, the popup itself takes focus, as Base UI does for a touch open and as\n * the WAI-ARIA dialog pattern advises for long content: the dialog opens at its\n * top, a screen reader reads its title, and Tab moves on to that element. A\n * dialog whose first tabbable element shows keeps Base UI's default.\n */\nfunction kitInitialFocus(\n  popup: HTMLElement | null,\n  openType: string,\n): HTMLElement | true {\n  if (!popup) return true;\n  if (openType === \"touch\") return popup;\n  const first = firstTabbable(popup);\n  return first && !startsInView(first, popup) ? popup : true;\n}\n\nexport function DialogPopup(popupProps: DialogPopupProps): React.ReactElement {\n  // Command and TaskCalendar pass their internal profiles through\n  // KitDialogPopup (dialog-internal.ts), the same function. Only the default\n  // profile takes a size; the others set their own width.\n  const {\n    className,\n    children,\n    showCloseButton = true,\n    profile = \"default\",\n    size = \"sm\",\n    closeProps,\n    portalProps,\n    initialFocus,\n    ref: givenRef,\n    ...props\n  } = popupProps as KitDialogPopupProps;\n  const popupRef = React.useRef<HTMLDivElement | null>(null);\n  const ref = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      popupRef.current = node;\n      if (typeof givenRef === \"function\") givenRef(node);\n      else if (givenRef) givenRef.current = node;\n    },\n    [givenRef],\n  );\n  return (\n    <DialogPortal {...portalProps}>\n      <SiteplaneUIPortalScope>\n        <DialogBackdrop />\n        <DialogViewport\n          className={cn(profile === \"command\" && \"sp-command-viewport\")}\n        >\n          <DialogPrimitive.Popup\n            className={cn(\n              slotClass(\"dialog-popup\", props),\n              baseClass(\"dialog\", props),\n              profile === \"command\"\n                ? cn(\n                    surfaceClassNames.overlayBorderless,\n                    \"sp-command-popup relative flex min-h-0 min-w-0 flex-col\",\n                  )\n                : profile === \"composite\"\n                  ? surfaceClassNames.dialogOverlayCompositePopup\n                  : surfaceClassNames.dialogOverlayPopup,\n              \"outline-none *:data-[slot=form]:contents\",\n              // AlertDialog shares the popup surface; its own rule lays out\n              // its popup as a grid. Small screens show it at the bottom\n              // edge, full width, where it slides in. The width of each size\n              // is a design-rework.css rule.\n              profile === \"default\" &&\n                cn(\n                  \"flex max-h-full w-full\",\n                  surfaceClassNames.dialogOverlayPopupMobileStick,\n                ),\n              className,\n            )}\n            data-foundation=\"dialog\"\n            data-slot=\"dialog-popup\"\n            data-profile={profile}\n            data-width={profile === \"default\" ? size : undefined}\n            initialFocus={\n              initialFocus ??\n              ((openType) => kitInitialFocus(popupRef.current, openType))\n            }\n            ref={ref}\n            {...props}\n          >\n            {children}\n            {showCloseButton && (\n              // The corner close of every overlay: the 24 px overlay-dismiss\n              // Button 8 px inside the top-end corner. The header keeps its\n              // text clear of it (design-rework.css).\n              <DialogPrimitive.Close\n                aria-label=\"Close\"\n                className=\"absolute end-2 top-2\"\n                render={\n                  <KitButton\n                    profile=\"overlay-dismiss\"\n                    shape=\"square\"\n                    variant=\"ghost\"\n                  />\n                }\n                {...closeProps}\n              >\n                <XIcon />\n              </DialogPrimitive.Close>\n            )}\n          </DialogPrimitive.Popup>\n        </DialogViewport>\n      </SiteplaneUIPortalScope>\n    </DialogPortal>\n  );\n}\n\nexport function DialogHeader({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      slotClass(\"dialog-header\", props, render),\n      \"flex flex-col\",\n      className,\n    ),\n    \"data-slot\": \"dialog-header\",\n  };\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function DialogFooter({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  const defaultProps = {\n    className: cn(\n      // Actions that do not fit wrap onto another row with the same gap instead\n      // of overflowing a narrow popup.\n      slotClass(\"dialog-footer\", props, render),\n      \"flex flex-wrap\",\n      // A dialog without content puts 24 px between its text and its actions in\n      // every layout (S12): 8 px here plus the footer's own 16 px top padding.\n      \"[[data-slot=dialog-header]+&]:mt-2\",\n      className,\n    ),\n    \"data-slot\": \"dialog-footer\",\n  };\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render,\n  });\n}\n\nexport function DialogTitle({\n  className,\n  ...props\n}: WithStringClassName<DialogPrimitive.Title.Props>): React.ReactElement {\n  return (\n    <DialogPrimitive.Title\n      className={cn(\n        slotClass(\"dialog-title\", props),\n        \"font-heading wrap-anywhere\",\n        className,\n      )}\n      data-slot=\"dialog-title\"\n      {...props}\n    />\n  );\n}\n\nexport function DialogDescription({\n  className,\n  ...props\n}: WithStringClassName<DialogPrimitive.Description.Props>): React.ReactElement {\n  return (\n    <DialogPrimitive.Description\n      className={cn(\n        slotClass(\"dialog-description\", props),\n        \"wrap-anywhere\",\n        className,\n      )}\n      data-slot=\"dialog-description\"\n      {...props}\n    />\n  );\n}\n\nexport function DialogPanel({\n  className,\n  scrollFade = true,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\"> & {\n  scrollFade?: boolean;\n}): React.ReactElement {\n  const defaultProps = {\n    className: cn(slotClass(\"dialog-panel\", props, render), className),\n    \"data-slot\": \"dialog-panel\",\n  };\n  return (\n    <ScrollArea scrollFade={scrollFade}>\n      {useRender({\n        defaultTagName: \"div\",\n        props: mergeProps<\"div\">(defaultProps, props),\n        render,\n      })}\n    </ScrollArea>\n  );\n}\n\n// DialogOverlay and DialogContent are the shadcn names of DialogBackdrop and\n// DialogPopup.\nexport {\n  DialogPrimitive,\n  DialogBackdrop as DialogOverlay,\n  DialogPopup as DialogContent,\n};\n","type":"registry:ui","target":"~/src/siteplane/components/dialog/dialog.tsx"},{"path":"packages/ui/src/components/dialog/dialog-internal.ts","content":"import type * as React from \"react\";\n\nimport { DialogPopup } from \"./dialog\";\n\n/*\n * Kit-internal DialogPopup. Not public API: the dialog index does not export\n * this module, and its names and profiles may change in any release. Command\n * renders its dialog with the command profile, and TaskCalendar its day dialog\n * with the composite profile.\n */\nexport type DialogPopupProfile = \"composite\" | \"default\" | \"command\";\n\nexport type KitDialogPopupProps = React.ComponentProps<typeof DialogPopup> & {\n  profile?: DialogPopupProfile;\n};\n\n/** DialogPopup with the kit's composition profiles; the same component. */\nexport const KitDialogPopup = DialogPopup as (\n  props: KitDialogPopupProps,\n) => React.ReactElement;\n","type":"registry:ui","target":"~/src/siteplane/components/dialog/dialog-internal.ts"},{"path":"packages/ui/src/components/dialog/dialog.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  /* The motion of the Dialog popup and its backdrop belongs to\n   * design-rework.css, which Dialog and AlertDialog share. */\n\n  /* Global search uses the public Dialog command profile, including its\n   * motion: it appears within 0.8 of the micro step and leaves within half of\n   * it, almost at once. */\n  :root .sp-command-viewport {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: var(--sp-rw-space-4);\n  }\n  :root .sp-base-dialog[data-profile=\"command\"] {\n    width: min(\n      calc(100dvw - var(--sp-rw-space-12) - var(--sp-rw-space-12)),\n      40rem\n    );\n    height: min(30rem, calc(100dvh - var(--sp-rw-space-8)));\n    max-width: none;\n    max-height: none;\n    overflow: hidden;\n    grid-row: 1;\n    border-radius: var(--sp-rw-radius-dialog);\n    transform: none;\n    transition: opacity calc(var(--motion-duration-micro) * 0.8)\n      var(--motion-ease-linear);\n  }\n\n  :root .sp-base-dialog[data-profile=\"command\"][data-starting-style],\n  :root .sp-base-dialog[data-profile=\"command\"][data-ending-style] {\n    opacity: 0;\n    transform: none;\n  }\n\n  :root .sp-base-dialog[data-profile=\"command\"][data-ending-style] {\n    transition-duration: calc(var(--motion-duration-micro) / 2);\n  }\n\n  @media (min-width: 64rem) {\n    :root .sp-base-dialog[data-profile=\"command\"] {\n      width: min(\n        calc(100dvw - var(--sp-rw-space-12) - var(--sp-rw-space-12)),\n        48rem\n      );\n    }\n  }\n\n  /* Reduced motion shows and hides the search at once (M5). The provider\n   * preset ranks above the profile rule, as the document mode does. */\n  @media (prefers-reduced-motion: reduce) {\n    :root .sp-base-dialog[data-profile=\"command\"] {\n      transition: none;\n    }\n  }\n\n  html[data-motion-reduce=\"true\"] .sp-base-dialog[data-profile=\"command\"],\n  :root [data-sp-motion=\"reduced\"] .sp-base-dialog[data-profile=\"command\"] {\n    transition: none;\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/dialog/dialog.css"},{"path":"packages/ui/src/components/dialog/index.ts","content":"export * from \"./dialog\";\n","type":"registry:ui","target":"~/src/siteplane/components/dialog/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/dialog.md","categories":["Overlays & disclosure"],"meta":{"avoidWhen":["Avoid for a decision that must be made before continuing; use Alert Dialog.","Avoid for short contextual content beside a trigger; use Popover."],"description":"Base UI-backed modal task boundary.","knownLimitations":["Do not wrap a submit button in DialogClose: it closes the dialog before the submission has succeeded."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["dialog"],"useWhen":["Use for a focused task or a form in an overlay, with a title and a clear close action."]}}