{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confirm-dialog",
  "title": "Confirm Dialog",
  "author": "AniBT <https://anibt.net>",
  "description": "One-call confirmation on @anibt/alert-dialog: red mark and danger key for destructive actions, and an onConfirm promise that keeps the dialog open and busy until it settles.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@anibt/theme",
    "@anibt/alert-dialog",
    "@anibt/spinner"
  ],
  "files": [
    {
      "path": "registry/anibt/ui/confirm-dialog.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { TriangleAlertIcon } from \"lucide-react\"\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogMedia,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@/registry/anibt/ui/alert-dialog\"\nimport { Spinner } from \"@/registry/anibt/ui/spinner\"\n\n/**\n * ConfirmDialog — AniBT 「贴纸手账」: the one way to ask before a destructive\n * or irreversible action, built on `@anibt/alert-dialog`.\n *\n * The footer is one order everywhere: the quiet 取消 on the left, the confirm\n * on the right — candy, or for `destructive` the red danger key, with A's red\n * mark in the bar.\n *\n * Closing:\n * - `onConfirm` returning nothing closes the dialog at once.\n * - `onConfirm` returning a promise keeps it open and busy until it settles:\n *   it closes on resolve and stays open on reject, so a failed delete leaves\n *   the reader where they can read the error and retry.\n * - A caller that owns the pending flag can pass `loading`; the confirm then\n *   shows the spinner and refuses further clicks.\n */\ntype ConfirmDialogProps = {\n  /** Controlled open state. Omit when using `trigger`. */\n  open?: boolean\n  onOpenChange?: (open: boolean) => void\n  /** The element that opens the dialog (uncontrolled use). */\n  trigger?: React.ReactNode\n  title: React.ReactNode\n  description?: React.ReactNode\n  /** Extra content between the bar and the buttons. */\n  children?: React.ReactNode\n  confirmLabel: React.ReactNode\n  cancelLabel?: React.ReactNode\n  /** The red confirm and the red mark. Deletes and revokes are destructive. */\n  destructive?: boolean\n  /** The icon in the bar's mark; a destructive dialog defaults to a warning sign. */\n  icon?: React.ReactNode\n  /** External pending state, merged with the promise `onConfirm` returns. */\n  loading?: boolean\n  confirmDisabled?: boolean\n  onConfirm: () => void | Promise<unknown>\n}\n\nfunction ConfirmDialog({\n  open,\n  onOpenChange,\n  trigger,\n  title,\n  description,\n  children,\n  confirmLabel,\n  cancelLabel = \"取消\",\n  destructive = false,\n  icon,\n  loading = false,\n  confirmDisabled = false,\n  onConfirm,\n}: ConfirmDialogProps) {\n  const [pending, setPending] = React.useState(false)\n  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)\n  const busy = loading || pending\n  const isOpen = open ?? uncontrolledOpen\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      if (open === undefined) setUncontrolledOpen(next)\n      onOpenChange?.(next)\n    },\n    [open, onOpenChange]\n  )\n\n  const handleConfirm = (event: React.MouseEvent<HTMLButtonElement>) => {\n    if (busy) {\n      event.preventDefault()\n      return\n    }\n    const result = onConfirm()\n    if (!isPromiseLike(result)) return\n\n    // Radix closes an alert dialog from its action unless the click's default\n    // is prevented: keep it open while the work runs.\n    event.preventDefault()\n    setPending(true)\n    void Promise.resolve(result).then(\n      () => {\n        setPending(false)\n        setOpen(false)\n      },\n      () => {\n        setPending(false)\n      }\n    )\n  }\n\n  const mark = icon ?? (destructive ? <TriangleAlertIcon /> : null)\n\n  return (\n    <AlertDialog open={isOpen} onOpenChange={setOpen}>\n      {trigger ? <AlertDialogTrigger {...{ asChild: true }}>{trigger}</AlertDialogTrigger> : null}\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          {mark ? <AlertDialogMedia tone={destructive ? \"destructive\" : \"accent\"}>{mark}</AlertDialogMedia> : null}\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          {description ? <AlertDialogDescription>{description}</AlertDialogDescription> : null}\n        </AlertDialogHeader>\n        {children}\n        <AlertDialogFooter>\n          <AlertDialogCancel disabled={busy}>{cancelLabel}</AlertDialogCancel>\n          <AlertDialogAction\n            variant={destructive ? \"destructive\" : \"default\"}\n            disabled={busy || confirmDisabled}\n            aria-busy={busy || undefined}\n            onClick={handleConfirm}\n          >\n            {busy ? <Spinner className=\"size-4\" /> : null}\n            {confirmLabel}\n          </AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n\nfunction isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n  return typeof value === \"object\" && value !== null && typeof (value as PromiseLike<unknown>).then === \"function\"\n}\n\nexport { ConfirmDialog }\nexport type { ConfirmDialogProps }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "overlay"
  ],
  "type": "registry:ui"
}