{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "confirm-dialog",
"title": "ConfirmDialog",
"description": "Confirm an irreversible step; asks for a reason when the API needs one; refusals shown in place.",
"dependencies": [
"@radix-ui/themes"
],
"files": [
{
"path": "src/components/confirm-dialog/ConfirmDialog.tsx",
"content": "/**\n * ConfirmDialog — \"are you sure\", for the steps that cannot be taken back or that the next person reads:\n * disqualifying a lead, losing a deal, blocking a partner. When the step needs a reason (the API refuses one\n * without it), the dialog asks for it and will not confirm while it is empty.\n *\n * The dialog stays open while the call runs and shows the API's refusal in place, so a person never loses\n * what they typed to an error.\n */\nimport { AlertDialog, Button, Callout, Flex, Text, TextArea } from \"@radix-ui/themes\";\nimport { useState, type ReactNode } from \"react\";\n\nexport interface ConfirmDialogProps {\n /** The button that opens the dialog. */\n trigger: ReactNode;\n title: string;\n description?: ReactNode;\n confirmLabel: string;\n tone?: \"danger\" | \"primary\";\n /** Ask for a reason; the label of its field, e.g. \"Why is it lost?\". */\n reason?: { label: string; placeholder?: string };\n /** Throw to keep the dialog open with the error shown. */\n onConfirm: (reason: string) => Promise<unknown> | unknown;\n}\n\nexport function ConfirmDialog({ trigger, title, description, confirmLabel, tone = \"primary\", reason, onConfirm }: ConfirmDialogProps) {\n const [open, setOpen] = useState(false);\n const [text, setText] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const blocked = !!reason && text.trim() === \"\";\n\n const confirm = async () => {\n setBusy(true);\n setError(null);\n try {\n await onConfirm(text.trim());\n setOpen(false);\n setText(\"\");\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <AlertDialog.Root open={open} onOpenChange={(o) => { setOpen(o); if (!o) setError(null); }}>\n <AlertDialog.Trigger>{trigger}</AlertDialog.Trigger>\n <AlertDialog.Content maxWidth=\"460px\">\n <AlertDialog.Title>{title}</AlertDialog.Title>\n <AlertDialog.Description size=\"2\">{description ?? \"This is recorded with your name.\"}</AlertDialog.Description>\n {reason && (\n <Flex direction=\"column\" gap=\"1\" mt=\"3\" asChild>\n <label>\n <Text size=\"2\" weight=\"medium\">{reason.label}</Text>\n <TextArea value={text} onChange={(e) => setText(e.target.value)} placeholder={reason.placeholder} required />\n </label>\n </Flex>\n )}\n {error && (\n <Callout.Root color=\"red\" role=\"alert\" mt=\"3\" size=\"1\"><Callout.Text>{error}</Callout.Text></Callout.Root>\n )}\n <Flex gap=\"3\" mt=\"4\" justify=\"end\">\n <AlertDialog.Cancel><Button variant=\"soft\" color=\"gray\">Cancel</Button></AlertDialog.Cancel>\n <Button color={tone === \"danger\" ? \"red\" : undefined} onClick={confirm} disabled={blocked} loading={busy}>\n {confirmLabel}\n </Button>\n </Flex>\n </AlertDialog.Content>\n </AlertDialog.Root>\n );\n}\n",
"type": "registry:component",
"target": "erp-ui/components/confirm-dialog/ConfirmDialog.tsx"
}
],
"type": "registry:component"
}