{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dialog",
  "title": "Dialog",
  "description": "A dialog component.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "@cubby-ui/button",
    "@cubby-ui/scroll-area",
    "@cubby-ui/elevated"
  ],
  "files": [
    {
      "path": "registry/default/dialog/dialog.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Dialog as BaseDialog } from \"@base-ui/react/dialog\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/default/button/button\";\nimport {\n  elevatedSurface,\n  type SurfaceLevel,\n} from \"@/registry/default/lib/elevated\";\nimport {\n  ScrollArea,\n  type ScrollAreaProps,\n} from \"@/registry/default/scroll-area/scroll-area\";\n\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { Cancel01Icon } from \"@hugeicons/core-free-icons\";\n/**\n * `\"inside\"` caps the popup to the viewport and scrolls `DialogBody`.\n * `\"outside\"` lets the popup grow past the viewport and scrolls the area\n * around it, so the whole card (header and footer included) moves.\n */\ntype DialogScroll = \"inside\" | \"outside\";\n\ninterface DialogConfigContextValue {\n  modal: boolean | \"trap-focus\";\n}\n\nconst DialogConfigContext = React.createContext<DialogConfigContextValue>({\n  modal: true,\n});\n\nconst DialogScrollContext = React.createContext<DialogScroll>(\"inside\");\n\nfunction Dialog<Payload>({\n  modal = true,\n  disablePointerDismissal,\n  ...props\n}: BaseDialog.Root.Props<Payload>) {\n  const configValue = React.useMemo(() => ({ modal }), [modal]);\n  return (\n    <DialogConfigContext.Provider value={configValue}>\n      <BaseDialog.Root\n        modal={modal}\n        disablePointerDismissal={disablePointerDismissal ?? modal !== true}\n        {...props}\n      />\n    </DialogConfigContext.Provider>\n  );\n}\n\nconst createDialogHandle = BaseDialog.createHandle;\n\nfunction DialogPortal({ ...props }: BaseDialog.Portal.Props) {\n  return <BaseDialog.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogTrigger({ ...props }: BaseDialog.Trigger.Props) {\n  return <BaseDialog.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogClose({ ...props }: BaseDialog.Close.Props) {\n  return <BaseDialog.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogBackdrop({ className, ...props }: BaseDialog.Backdrop.Props) {\n  return (\n    <BaseDialog.Backdrop\n      className={cn(\n        \"ease-out-expo fixed inset-0 min-h-dvh bg-black/40 transition-all duration-200 supports-[-webkit-touch-callout:none]:absolute\",\n        \"backdrop-blur-sm data-ending-style:opacity-0 data-starting-style:opacity-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogViewport({\n  className,\n  scroll = \"inside\",\n  children,\n  ...props\n}: BaseDialog.Viewport.Props & { scroll?: DialogScroll }) {\n  const { modal } = React.useContext(DialogConfigContext);\n  return (\n    <BaseDialog.Viewport\n      data-slot=\"dialog-viewport\"\n      className={cn(\n        \"fixed inset-0\",\n        // Inside scroll: the viewport is the centering frame the popup is\n        // capped against. Outside scroll: it's only the scroll port, and the\n        // ScrollArea below owns the centering and padding so the popup can\n        // grow past the bottom edge.\n        scroll === \"inside\" &&\n          \"flex flex-col items-center justify-center overflow-hidden px-4 py-6\",\n        className,\n      )}\n      {...props}\n    >\n      <DialogScrollContext.Provider value={scroll}>\n        {scroll === \"outside\" ? (\n          <ScrollArea\n            // `min-h-full` (not `h-full`) keeps the centering box growable, so a\n            // short popup sits centered and a tall one extends the scroll range\n            // instead of having its top clipped out of reach.\n            contentClassName=\"flex min-h-full items-center justify-center px-4 py-6\"\n            // A non-modal viewport is `pointer-events-none` so the page stays\n            // usable, which would otherwise leave the scrollbar thumb dead.\n            // Only the thumb opts back in; the gutter keeps passing wheel and\n            // clicks through to the page.\n            className={cn(\n              modal !== true &&\n                \"[&_[data-slot=scroll-area-scrollbar]]:pointer-events-auto\",\n            )}\n          >\n            {children}\n          </ScrollArea>\n        ) : (\n          children\n        )}\n      </DialogScrollContext.Provider>\n    </BaseDialog.Viewport>\n  );\n}\n\nfunction DialogContent({\n  className,\n  children,\n  showCloseButton = true,\n  variant = \"default\",\n  scroll = \"inside\",\n  level = 5,\n  shadowLevel = 5,\n  ref,\n  initialFocus,\n  ...props\n}: BaseDialog.Popup.Props & {\n  showCloseButton?: boolean;\n  variant?: \"default\" | \"inset\";\n  /**\n   * Whether the dialog body scrolls inside the popup (`\"inside\"`) or the area\n   * around the popup scrolls (`\"outside\"`). Treat as fixed for the dialog's\n   * lifetime: changing it while open swaps the element tree above the popup,\n   * which remounts it and replays the entry animation.\n   */\n  scroll?: DialogScroll;\n  /** Surface elevation level for the dialog bg (1-8). Defaults to 5 — the standard \"dialog tier\" above page/cards/popovers. Bump to 7 for a hero/critical modal. */\n  level?: SurfaceLevel;\n  /** Shadow weight (1-8). Pinned to 5 by default — matches the dialog tier, dramatic enough to anchor the modal without overpowering. Bump to 7 for hero modals. */\n  shadowLevel?: SurfaceLevel;\n}) {\n  const { modal } = React.useContext(DialogConfigContext);\n  const isModal = modal === true;\n  const isOutsideScroll = scroll === \"outside\";\n\n  // Outside scroll points `initialFocus` at the popup itself rather than the\n  // first focusable child, so opening a tall dialog doesn't scroll straight\n  // past the header. That needs our own handle on the element, kept alongside\n  // any ref the caller passed.\n  const popupRef = React.useRef<HTMLDivElement | null>(null);\n  const mergedRef = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      popupRef.current = node;\n      if (typeof ref !== \"function\") {\n        if (ref) ref.current = node;\n        return () => {\n          popupRef.current = null;\n          if (ref) ref.current = null;\n        };\n      }\n      // React 19 ref callbacks may return a cleanup. Returning one here makes\n      // React run it on detach instead of calling us back with `null`, so the\n      // caller's cleanup has to be forwarded rather than dropped.\n      const cleanup = ref(node);\n      return () => {\n        popupRef.current = null;\n        if (typeof cleanup === \"function\") cleanup();\n        else ref(null);\n      };\n    },\n    [ref],\n  );\n\n  const popup = (\n    <BaseDialog.Popup\n      ref={mergedRef}\n      initialFocus={initialFocus ?? (isOutsideScroll ? popupRef : undefined)}\n      data-slot=\"dialog-content\"\n      data-variant={variant}\n      data-level={level}\n      data-scroll={scroll}\n      className={cn(\n        \"text-popover-foreground relative z-50 flex w-full max-w-full min-w-0 flex-col overflow-hidden\",\n        \"rounded-2xl sm:max-w-lg\",\n        scroll === \"inside\" && \"max-h-full min-h-0\",\n        // Outside scroll parks initial focus on the popup itself, so suppress\n        // the UA ring it would otherwise draw around the whole card.\n        isOutsideScroll && \"outline-none\",\n        // Surface elevation — bg + shadow + rim overlay (rim uses ::after at z-[2])\n        elevatedSurface(level, shadowLevel),\n        // Mobile: bottom sheet style\n        // \"right-0 bottom-0 left-0 rounded-t-lg\",\n        // Desktop: centered modal\n        \"-translate-y-[calc(1.25rem*var(--nested-dialogs))]\",\n\n        // Scale effect for nested dialogs on desktop\n        \"scale-[calc(1-0.1*var(--nested-dialogs))]\",\n        // Animation duration\n        \"ease-out-expo transition-all duration-200\",\n        // Desktop animations: scale and fade\n        \"data-starting-style:translate-y-[calc(1.25rem)] data-starting-style:scale-95 data-starting-style:opacity-0\",\n        \"data-ending-style:translate-y-[calc(1.25rem)] data-ending-style:scale-95 data-ending-style:opacity-0\",\n        // Nested dialog overlay — uses ::before (not ::after, that's the rim) at z-[3] so it paints above content AND above the rim\n        \"before:pointer-events-none before:absolute before:inset-0 before:z-3 before:hidden before:rounded-[inherit] before:bg-black/5 before:opacity-0 before:transition-[opacity,display] before:transition-discrete before:duration-200\",\n        \"data-nested-dialog-open:before:block data-nested-dialog-open:before:opacity-100\",\n        \"starting:data-nested-dialog-open:before:opacity-0\",\n        !isModal && \"pointer-events-auto\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      {/* Pinned to the card, not the viewport, in both modes. Under outside\n          scroll a tall card carries it off screen; Escape and the footer\n          remain. Matches Base UI's own outside-scroll demo. */}\n      {showCloseButton && (\n        <DialogClose\n          aria-label=\"Close\"\n          className=\"absolute end-2 top-2\"\n          render={<Button size=\"icon_sm\" variant=\"ghost\" />}\n        >\n          <HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />\n        </DialogClose>\n      )}\n    </BaseDialog.Popup>\n  );\n\n  return (\n    <DialogPortal>\n      {isModal && <DialogBackdrop />}\n      <DialogViewport\n        scroll={scroll}\n        className={cn(!isModal && \"pointer-events-none\")}\n      >\n        {popup}\n      </DialogViewport>\n    </DialogPortal>\n  );\n}\n\n// Slot-presence padding uses ancestor `:has()` on `[data-slot=dialog-content]`\n// so header/body/footer can be wrapped in forms, ErrorBoundaries, or fragments\n// without breaking spacing — adjacent-sibling selectors wouldn't reach through.\nfunction DialogHeader({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      data-slot=\"dialog-header\"\n      className={cn(\n        \"flex flex-col gap-2 px-6 pt-6 pb-3\",\n        // Header alone with footer (no body anywhere in content)\n        \"in-[[data-slot=dialog-content]:not(:has([data-slot=dialog-body])):has([data-slot=dialog-footer])]:pb-6\",\n        // Header alone (no body, no footer)\n        \"in-[[data-slot=dialog-content]:not(:has([data-slot=dialog-body])):not(:has([data-slot=dialog-footer]))]:pb-6\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogBody({\n  className,\n  nativeScroll = false,\n  fadeEdges = true,\n  scrollbarGutter = false,\n  persistScrollbar,\n  hideScrollbar,\n  children,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement> & {\n  nativeScroll?: boolean;\n} & Pick<\n    ScrollAreaProps,\n    \"fadeEdges\" | \"scrollbarGutter\" | \"persistScrollbar\" | \"hideScrollbar\"\n  >) {\n  const scroll = React.useContext(DialogScrollContext);\n\n  if (process.env.NODE_ENV !== \"production\") {\n    if (\n      scroll === \"outside\" &&\n      (nativeScroll ||\n        fadeEdges !== true ||\n        scrollbarGutter ||\n        persistScrollbar !== undefined ||\n        hideScrollbar !== undefined)\n    ) {\n      console.error(\n        'DialogBody: `fadeEdges`, `scrollbarGutter`, `persistScrollbar`, `hideScrollbar` and `nativeScroll` have no effect under `scroll=\"outside\"` — the body renders no scroller of its own there.',\n      );\n    }\n  }\n\n  const content = (\n    <div className={cn(\"px-6 py-1\", className)} {...props}>\n      {children}\n    </div>\n  );\n\n  return (\n    <div\n      data-slot=\"dialog-body\"\n      className={cn(\n        // Outside scroll has no scroller here to fill — the body is sized by\n        // its content and the area around the popup scrolls instead.\n        scroll === \"inside\" && \"flex min-h-0 flex-1 flex-col\",\n        // No header anywhere in content → body needs its own top padding\n        \"in-[[data-slot=dialog-content]:not(:has([data-slot=dialog-header]))]:pt-5\",\n        // No footer anywhere in content → body needs its own bottom padding\n        \"in-[[data-slot=dialog-content]:not(:has([data-slot=dialog-footer]))]:pb-5\",\n        // Inset variant: still need bottom padding when a footer follows so the\n        // body's content doesn't crowd the footer's top border\n        \"in-data-[variant=inset]:in-[[data-slot=dialog-content]:has([data-slot=dialog-footer])]:pb-5\",\n      )}\n    >\n      {scroll === \"outside\" ? (\n        content\n      ) : (\n        <ScrollArea\n          className=\"flex-1\"\n          fadeEdges={fadeEdges}\n          scrollbarGutter={scrollbarGutter}\n          persistScrollbar={persistScrollbar}\n          hideScrollbar={hideScrollbar}\n          nativeScroll={nativeScroll}\n        >\n          {content}\n        </ScrollArea>\n      )}\n    </div>\n  );\n}\n\nfunction DialogFooter({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      data-slot=\"dialog-footer\"\n      className={cn(\n        \"flex flex-col-reverse gap-2 px-6 pt-4 pb-6 sm:flex-row sm:justify-end\",\n        // No header AND no body → footer alone, needs its own top padding\n        \"in-[[data-slot=dialog-content]:not(:has([data-slot=dialog-header])):not(:has([data-slot=dialog-body]))]:pt-6\",\n        // Reduce top padding when body is present\n        \"not-in-data-[variant=inset]:in-[[data-slot=dialog-content]:has([data-slot=dialog-body])]:pt-3\",\n        // Inset variant: muted background with top border for separation\n        \"in-data-[variant=inset]:border-border in-data-[variant=inset]:bg-muted in-data-[variant=inset]:rounded-b-2xl in-data-[variant=inset]:border-t in-data-[variant=inset]:pt-4 in-data-[variant=inset]:pb-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogTitle({ className, ...props }: BaseDialog.Title.Props) {\n  return (\n    <BaseDialog.Title\n      className={cn(\n        \"text-foreground text-lg leading-none font-semibold tracking-tight text-balance\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogDescription({\n  className,\n  ...props\n}: BaseDialog.Description.Props) {\n  return (\n    <BaseDialog.Description\n      className={cn(\"text-muted-foreground text-sm text-pretty\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Dialog,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogBody,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n  DialogClose,\n  DialogPortal,\n  DialogBackdrop,\n  DialogViewport,\n  createDialogHandle,\n};\nexport type { DialogScroll };\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/dialog.tsx"
    }
  ],
  "type": "registry:ui"
}