{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stacked-dialog",
  "type": "registry:ui",
  "title": "Stacked Dialog",
  "description": "Each dialog opened from another pushes it back, dimmed, with a way back.",
  "categories": [
    "dialogs"
  ],
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "components/ui/stacked-dialog.tsx",
      "content": "\"use client\";\nimport { Dialog as BaseDialog } from \"@base-ui/react/dialog\";\nimport { createContext, useContext, useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ChevronLeft, X } from \"@/lib/icons\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\ntype Stack = {\n  /** 1 for the first dialog, 2 for one opened from it, and so on. */\n  depth: number;\n  /** Title of the dialog this one was opened from, for the back button. */\n  parentTitle: React.ReactNode | null;\n  register: (close: () => void) => () => void;\n  closeAll: () => void;\n  closeSelf: () => void;\n};\nconst StackCtx = createContext<Stack | null>(null);\n\ntype Level = { depth: number; title: React.ReactNode | null; close: () => void };\nconst LevelCtx = createContext<Level | null>(null);\n\nexport type StackedDialogProps = Omit<BaseDialog.Root.Props, \"open\" | \"defaultOpen\" | \"onOpenChange\"> & {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\n/**\n * One level of a stack. Nest another StackedDialog inside a StackedDialogContent\n * and opening it pushes this one back; closing it brings this one forward again.\n */\nexport function StackedDialog({ open: openProp, defaultOpen = false, onOpenChange, children, ...rest }: StackedDialogProps) {\n  const outer = useContext(StackCtx);\n  const level = useContext(LevelCtx);\n  const [open, setOpen] = useControllableState({ value: openProp, defaultValue: defaultOpen, onChange: onOpenChange });\n  const closers = useRef(new Set<() => void>());\n\n  const depth = (level?.depth ?? 0) + 1;\n  // The outermost level owns the registry; every level below registers with it.\n  const register = outer?.register ?? ((close: () => void) => {\n    closers.current.add(close);\n    return () => closers.current.delete(close);\n  });\n  const closeAll = outer?.closeAll ?? (() => {\n    for (const close of closers.current) close();\n    setOpen(false);\n  });\n\n  useEffect(() => {\n    if (!outer) return;\n    return outer.register(() => setOpen(false));\n  }, [outer, setOpen]);\n\n  return (\n    <StackCtx.Provider value={{ depth, parentTitle: level?.title ?? null, register, closeAll, closeSelf: () => setOpen(false) }}>\n      <BaseDialog.Root open={open} onOpenChange={(next) => setOpen(next)} {...rest}>\n        {children}\n      </BaseDialog.Root>\n    </StackCtx.Provider>\n  );\n}\n\n/** Depth, and ways out: close this level, or the whole stack at once (after a final save, say). */\nexport function useStackedDialog() {\n  const stack = useContext(StackCtx);\n  const level = useContext(LevelCtx);\n  return { depth: level?.depth ?? stack?.depth ?? 0, close: level?.close ?? (() => {}), closeAll: stack?.closeAll ?? (() => {}) };\n}\n\nexport type StackedDialogTriggerProps = BaseDialog.Trigger.Props;\n\nexport function StackedDialogTrigger(props: StackedDialogTriggerProps) {\n  return <BaseDialog.Trigger {...props} />;\n}\n\nexport type StackedDialogContentProps = Omit<BaseDialog.Popup.Props, \"className\" | \"title\"> & {\n  className?: string;\n  /** Names the dialog, and becomes the back label of any dialog opened from it. */\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Render into this element instead of document.body. */\n  container?: BaseDialog.Portal.Props[\"container\"];\n  closeLabel?: string;\n};\n\nconst widths = { sm: \"max-w-[400px]\", md: \"max-w-[480px]\", lg: \"max-w-[640px]\" };\n\nexport function StackedDialogContent({\n  title,\n  description,\n  size = \"md\",\n  container,\n  closeLabel = \"Close\",\n  className,\n  children,\n  ...rest\n}: StackedDialogContentProps) {\n  const stack = useContext(StackCtx);\n  const depth = stack?.depth ?? 1;\n  const parentTitle = stack?.parentTitle ?? null;\n  const contained = container != null;\n\n  return (\n    <BaseDialog.Portal container={container}>\n      {/* Only the first level draws a backdrop; the ones above dim the dialog they cover instead. */}\n      <BaseDialog.Backdrop\n        className={cn(\n          contained ? \"absolute\" : \"fixed\",\n          \"inset-0 z-(--z-overlay) bg-overlay transition-opacity duration-200 ease-out-quart\",\n          \"data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:duration-150\",\n        )}\n      />\n      <BaseDialog.Viewport\n        className={cn(\n          contained ? \"absolute\" : \"fixed\",\n          // Top-anchored so every level shares a top edge and the ones behind peek above it.\n          \"inset-0 z-(--z-dialog) flex items-start justify-center px-4 pt-[max(48px,12vh)] pb-4\",\n        )}\n      >\n        <BaseDialog.Popup\n          data-depth={depth}\n          className={cn(\n            \"group/stack relative flex max-h-[calc(100%-8px)] w-full min-w-0 flex-col rounded-2xl border border-line-2 bg-raised text-fg shadow-pop outline-none\",\n            widths[size],\n            \"origin-top\",\n            // Pushed back by each level opened above it: rises 12px and shrinks 5% per level.\n            \"translate-y-[calc(var(--nested-dialogs,0)*-12px)] scale-[calc(1-var(--nested-dialogs,0)*0.05)]\",\n            \"transition-[opacity,scale,translate] duration-[280ms] ease-out-expo\",\n            // A new level arrives from 8px below; leaving, it drops back 4px and fades, faster.\n            \"data-starting-style:translate-y-2 data-starting-style:scale-[0.98] data-starting-style:opacity-0\",\n            \"data-ending-style:translate-y-1 data-ending-style:opacity-0 data-ending-style:duration-[180ms] data-ending-style:ease-out-quart\",\n            // More than two levels back, a dialog fades out of the pile entirely.\n            \"data-nested-dialog-open:opacity-[calc(1-max(0,var(--nested-dialogs,0)-2))]\",\n            // The dim that sits on a pushed-back level.\n            \"after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-overlay after:opacity-0 after:transition-opacity after:duration-[280ms] data-nested-dialog-open:after:opacity-100\",\n            className,\n          )}\n          {...rest}\n        >\n          <LevelCtx.Provider value={{ depth, title, close: () => stack?.closeSelf() }}>\n            <div className=\"flex shrink-0 flex-col gap-1 px-5 pb-3 pt-4 pr-12\">\n              {depth > 1 && parentTitle != null && (\n                <BaseDialog.Close\n                  className={cn(\n                    \"group/back relative -ml-1.5 mb-1 inline-flex h-6 max-w-full items-center gap-0.5 self-start rounded-md pl-0.5 pr-1.5 text-[12px] text-fg-3\",\n                    \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n                    \"transition-[background-color,color,scale] duration-150 ease-out-quart hover:bg-hover hover:text-fg active:scale-[0.97]\",\n                    \"before:absolute before:-inset-y-2.5 before:inset-x-0 before:content-[''] pointer-fine:before:hidden\",\n                  )}\n                >\n                  <ChevronLeft size={14} className=\"shrink-0 transition-transform duration-150 ease-out-quart group-hover/back:-translate-x-0.5\" />\n                  <span className=\"sr-only\">Back to </span>\n                  <span className=\"truncate\">{parentTitle}</span>\n                </BaseDialog.Close>\n              )}\n              <BaseDialog.Title className=\"text-[15px] font-medium leading-[1.3] tracking-[-0.015em] text-fg text-balance\">{title}</BaseDialog.Title>\n              {description && <BaseDialog.Description className=\"text-[13px] leading-[1.5] text-fg-2 text-pretty\">{description}</BaseDialog.Description>}\n            </div>\n            {children}\n          </LevelCtx.Provider>\n          <BaseDialog.Close\n            aria-label={closeLabel}\n            onClick={() => {\n              // The × on a nested level leaves the whole flow; the back link steps down one level.\n              if (depth > 1) stack?.closeAll();\n            }}\n            className={cn(\n              \"absolute right-3 top-3 grid size-7 place-items-center rounded-md text-fg-3\",\n              \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n              \"transition-[background-color,color,scale] duration-150 ease-out-quart hover:bg-hover hover:text-fg active:scale-[0.92] active:duration-75\",\n              \"before:absolute before:-inset-2 before:content-[''] pointer-fine:before:hidden\",\n            )}\n          >\n            <X size={16} />\n          </BaseDialog.Close>\n        </BaseDialog.Popup>\n      </BaseDialog.Viewport>\n    </BaseDialog.Portal>\n  );\n}\n\nexport type StackedDialogBodyProps = React.ComponentProps<\"div\">;\n\nexport function StackedDialogBody({ className, ...rest }: StackedDialogBodyProps) {\n  return <div className={cn(\"min-h-0 flex-1 overflow-y-auto overscroll-contain px-5 py-1\", className)} {...rest} />;\n}\n\nexport type StackedDialogFooterProps = React.ComponentProps<\"div\">;\n\nexport function StackedDialogFooter({ className, ...rest }: StackedDialogFooterProps) {\n  return (\n    <div\n      className={cn(\"flex shrink-0 items-center justify-end gap-2 px-5 pb-5 pt-4 max-sm:grid max-sm:auto-cols-fr max-sm:grid-flow-col max-sm:*:h-10\", className)}\n      {...rest}\n    />\n  );\n}\n\nexport type StackedDialogCloseProps = Omit<BaseDialog.Close.Props, \"className\"> & {\n  className?: string;\n  variant?: \"primary\" | \"secondary\" | \"ghost\";\n};\n\n/** Closes this level only, returning to the one below. */\nexport function StackedDialogClose({ variant = \"secondary\", className, ...rest }: StackedDialogCloseProps) {\n  return (\n    <BaseDialog.Close\n      className={cn(\n        \"relative inline-flex h-8 shrink-0 select-none items-center justify-center whitespace-nowrap rounded-lg px-2.5 text-[12.5px] font-medium tracking-[-0.005em]\",\n        \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n        \"transition-[background-color,border-color,color,scale] duration-150 ease-out-quart active:scale-[0.97] active:duration-75\",\n        \"data-disabled:pointer-events-none data-disabled:opacity-50\",\n        variant === \"primary\" && \"bg-fg text-frame shadow-[var(--shadow)] hover:bg-fg/90\",\n        variant === \"secondary\" && \"border border-line-2 bg-raised text-fg shadow-[var(--shadow)] hover:border-fg-4 hover:bg-hover\",\n        variant === \"ghost\" && \"text-fg-2 hover:bg-hover hover:text-fg\",\n        className,\n      )}\n      {...rest}\n    />\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/stacked-dialog"
}
