{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confirm-dialog",
  "type": "registry:component",
  "title": "Confirm dialog",
  "description": "Confirmation before an act that takes something away or decides: what it does, an optional typed name or note, the error in place, an answer shown once.",
  "registryDependencies": [
    "@wibo/alert",
    "@wibo/button",
    "@wibo/dialog",
    "@wibo/input",
    "@wibo/label"
  ],
  "files": [
    {
      "path": "src/patterns/confirm-dialog.tsx",
      "type": "registry:component",
      "content": "import { isValidElement, useId, useRef, useState, type ReactElement, type ReactNode } from \"react\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\n\n/*\n * A confirmation before an act: always for one that takes something away\n * (remove, revoke, archive, rotate), and for a short decision that needs one\n * or two fields at most (a note). It says what the act does and what stays,\n * in the caller's words; `typed` wants the thing's own name typed before the\n * verb unlocks. `onConfirm` is the act: resolve and the dialog closes,\n * reject and the error's message shows inside the dialog, resolve to an\n * element and the dialog stays open showing it (an answer that cannot be read\n * again, such as a new secret) with one `doneLabel` button. Holds no copy.\n *\n * It holds state: render it from client components and import it by path\n * (`@wibo/ui/patterns/confirm-dialog`).\n */\n\ninterface ConfirmDialogProps {\n  /** The element that opens the dialog; omit when a row menu or other state opens it. */\n  trigger?: ReactNode;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  title: ReactNode;\n  /** What the act does and what stays. */\n  description?: ReactNode;\n  /** The verb on the confirm button. */\n  confirmLabel: ReactNode;\n  cancelLabel?: string;\n  closeLabel?: string;\n  /** The answer view's one button. */\n  doneLabel?: string;\n  /** `destructive` (default) for an act that takes something away, `default` for one that only changes or decides. */\n  tone?: \"destructive\" | \"default\";\n  /** The thing's own name, typed before the verb unlocks: for acts that cannot be undone. */\n  typed?: { label: ReactNode; expect: string };\n  /** One or two fields the act takes (a note); controlled by the caller. */\n  children?: ReactNode;\n  /** The act. Rejects with the message to show; may resolve to an element to show instead of closing. */\n  onConfirm: () => Promise<ReactElement | void> | ReactElement | void;\n}\n\nfunction ConfirmDialog({\n  trigger,\n  open: openProp,\n  onOpenChange,\n  title,\n  description,\n  confirmLabel,\n  cancelLabel = \"Cancel\",\n  closeLabel,\n  doneLabel = \"Done\",\n  tone = \"destructive\",\n  typed,\n  children,\n  onConfirm,\n}: ConfirmDialogProps) {\n  const [uncontrolled, setUncontrolled] = useState(false);\n  const [busy, setBusy] = useState(false);\n  // Opened by state (a row menu), the dialog has no trigger to return to: focus goes back to what opened it.\n  const returnFocus = useRef<HTMLElement | null>(null);\n  const open = openProp ?? uncontrolled;\n  const setOpen = (next: boolean) => {\n    // A running act keeps the dialog: its outcome lands here.\n    if (busy && !next) return;\n    if (openProp === undefined) setUncontrolled(next);\n    onOpenChange?.(next);\n  };\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      {trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}\n      <DialogContent\n        data-slot=\"confirm-dialog\"\n        className=\"max-h-[90dvh] overflow-y-auto sm:max-w-md\"\n        closeLabel={closeLabel}\n        onOpenAutoFocus={() => {\n          returnFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n        }}\n        onCloseAutoFocus={(event) => {\n          if (trigger || !returnFocus.current?.isConnected) return;\n          event.preventDefault();\n          returnFocus.current.focus();\n        }}\n        {...(description ? {} : { \"aria-describedby\": undefined })}\n      >\n        {/* The body unmounts with the content, so a typed confirmation or an error never survives into the next opening. */}\n        <ConfirmBody\n          title={title}\n          description={description}\n          confirmLabel={confirmLabel}\n          cancelLabel={cancelLabel}\n          doneLabel={doneLabel}\n          tone={tone}\n          typed={typed}\n          onConfirm={onConfirm}\n          busy={busy}\n          setBusy={setBusy}\n          onClose={() => {\n            if (openProp === undefined) setUncontrolled(false);\n            onOpenChange?.(false);\n          }}\n        >\n          {children}\n        </ConfirmBody>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\ninterface ConfirmBodyProps extends Pick<ConfirmDialogProps, \"title\" | \"description\" | \"confirmLabel\" | \"typed\" | \"onConfirm\" | \"children\"> {\n  cancelLabel: string;\n  doneLabel: string;\n  tone: \"destructive\" | \"default\";\n  busy: boolean;\n  setBusy: (busy: boolean) => void;\n  onClose: () => void;\n}\n\nfunction ConfirmBody({ title, description, confirmLabel, cancelLabel, doneLabel, tone, typed, onConfirm, busy, setBusy, onClose, children }: ConfirmBodyProps) {\n  const typedId = useId();\n  const [typedValue, setTypedValue] = useState(\"\");\n  const [error, setError] = useState<string | null>(null);\n  const [answer, setAnswer] = useState<ReactElement | null>(null);\n  const ready = typed === undefined || typedValue.trim() === typed.expect.trim();\n\n  const act = async () => {\n    if (!ready || busy) return;\n    setBusy(true);\n    setError(null);\n    try {\n      const shown = await onConfirm();\n      setBusy(false);\n      if (isValidElement(shown)) setAnswer(shown);\n      else onClose();\n    } catch (err) {\n      setBusy(false);\n      setError(err instanceof Error ? err.message : String(err));\n    }\n  };\n\n  if (answer !== null) {\n    return (\n      <div className=\"grid gap-4\">\n        <DialogHeader>\n          <DialogTitle>{title}</DialogTitle>\n        </DialogHeader>\n        {answer}\n        <DialogFooter>\n          <Button type=\"button\" onClick={onClose}>\n            {doneLabel}\n          </Button>\n        </DialogFooter>\n      </div>\n    );\n  }\n\n  return (\n    <form\n      className=\"grid gap-4\"\n      onSubmit={(event) => {\n        event.preventDefault();\n        void act();\n      }}\n    >\n      <DialogHeader>\n        <DialogTitle>{title}</DialogTitle>\n        {description ? <DialogDescription>{description}</DialogDescription> : null}\n      </DialogHeader>\n      {children}\n      {typed ? (\n        <div className=\"grid gap-1.5\">\n          <Label htmlFor={typedId}>{typed.label}</Label>\n          <Input id={typedId} value={typedValue} autoComplete=\"off\" spellCheck={false} placeholder={typed.expect} disabled={busy} onChange={(event) => setTypedValue(event.target.value)} />\n        </div>\n      ) : null}\n      {error ? (\n        <Alert variant=\"danger\" data-slot=\"confirm-dialog-error\">\n          <AlertDescription>{error}</AlertDescription>\n        </Alert>\n      ) : null}\n      <DialogFooter>\n        <Button type=\"button\" variant=\"outline\" disabled={busy} onClick={onClose}>\n          {cancelLabel}\n        </Button>\n        <Button type=\"submit\" variant={tone === \"destructive\" ? \"destructive\" : \"default\"} disabled={!ready} loading={busy}>\n          {confirmLabel}\n        </Button>\n      </DialogFooter>\n    </form>\n  );\n}\n\nexport { ConfirmDialog };\nexport type { ConfirmDialogProps };\n"
    }
  ]
}
