{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "type-to-confirm",
  "title": "Type-to-Confirm",
  "description": "The confirmation step in front of an irreversible action: the user has to type the resource's own name (\"acme-prod\") before the destructive button turns on. Use it wherever a misclick would be unrecoverable — deleting a project, repository, workspace, organisation, cluster, database, or environment, removing a team member, revoking an API key, wiping data, cancelling a subscription, or any \"danger zone\" section of a settings page. Common asks it answers: \"type to confirm\", \"type the project name to delete\", \"type DELETE to confirm\", \"confirm delete by typing name\", \"GitHub-style delete confirmation\", \"danger zone dialog\", \"destructive action modal\", \"disable the delete button until the name matches\". shadcn/ui ships alert-dialog as an empty shell — the typed match, the disabled-until-it-matches wiring, and the announcement are left to you every time; this packages them into one drop-in that sits inside your existing dialog or card, so nothing here assumes which official components you have installed. Pass `phrase` (the name) and `onConfirm`; both sides are trimmed before comparing, so a pasted name that picked up a trailing space still matches, and an empty phrase never matches, which stops an untouched field from arming a delete. Set `caseSensitive={false}` to let \"delete\" pass for \"DELETE\", and mirror your mutation with `pending` to lock the field and swap the button label. The field opts out of autocomplete, autocorrect, autocapitalisation, and spellcheck — on a phone the first letter would otherwise be capitalised and an exact match made impossible to type. The button is genuinely disabled rather than aria-disabled, which screen readers skip: nothing is lost by that, because the label states what to type, the description explains that the button is waiting for it, and an always-mounted live region announces the moment it turns on (a live region inserted together with its text is not reliably announced, so one that appeared only on match would swallow that update). It renders as a real <form>, so Enter submits and, inside a dialog, focus lands on the field on open with no extra wiring. Styled with shadcn tokens (border-input, destructive, muted-foreground, ring) for automatic light/dark theming, with zero dependencies beyond your cn util. Distinct from confirm-button, which is a two-step click for cheap, reversible actions; this is the high-friction guard for the ones you cannot take back.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/ui/type-to-confirm.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface TypeToConfirmProps\n  extends Omit<React.ComponentPropsWithoutRef<\"form\">, \"onSubmit\"> {\n  /**\n   * The exact text the user has to type back, normally the resource's own name\n   * (\"acme-prod\"). A name is what makes the gesture deliberate — a generic word\n   * like DELETE becomes muscle memory and gets typed into the wrong dialog.\n   */\n  phrase: string\n  /** Runs on submit, once what was typed matches `phrase`. */\n  onConfirm: () => void\n  /** Label above the field. Defaults to \"Type <phrase> to confirm\". */\n  label?: React.ReactNode\n  /**\n   * The sentence under the field. It explains why the button is disabled, and\n   * is what the field points at with `aria-describedby`.\n   */\n  description?: React.ReactNode\n  /** Text of the destructive submit button. */\n  actionLabel?: string\n  /** Button text while the request runs. */\n  pendingLabel?: string\n  /** Mirror of your mutation's in-flight state; locks the field and button. */\n  pending?: boolean\n  /** Compare exactly. Turn off to let \"delete\" pass for \"DELETE\". */\n  caseSensitive?: boolean\n}\n\n/**\n * The confirmation step in front of an irreversible action: the user has to\n * type the resource's name before the destructive button turns on. Drop it into\n * an alert dialog or a \"danger zone\" card.\n *\n * Both sides are trimmed before comparing, so a pasted name that picked up a\n * trailing space still matches, and an empty `phrase` never matches at all —\n * otherwise an untouched field would arm a delete. The field opts out of\n * autocomplete, autocorrect, autocapitalisation and spellcheck: on a phone the\n * first letter would otherwise be capitalised and an exact match made\n * impossible to type.\n *\n * The button is genuinely `disabled` rather than `aria-disabled`. A disabled\n * control is skipped by screen readers, which is normally a reason to avoid it,\n * but here nothing is hidden by that: the label states exactly what to type,\n * the description says the button is waiting for it, and a live region\n * announces the moment it turns on. For an action that cannot be undone,\n * refusing the click outright beats a button that looks armed and explains\n * itself only after it has been pressed.\n */\nexport function TypeToConfirm({\n  phrase,\n  onConfirm,\n  label,\n  description,\n  actionLabel = \"Delete\",\n  pendingLabel = \"Deleting…\",\n  pending = false,\n  caseSensitive = true,\n  className,\n  ...props\n}: TypeToConfirmProps) {\n  const inputId = React.useId()\n  const descriptionId = React.useId()\n  const [typed, setTyped] = React.useState(\"\")\n\n  const normalize = (value: string) =>\n    caseSensitive ? value.trim() : value.trim().toLowerCase()\n\n  const matches = phrase.trim() !== \"\" && normalize(typed) === normalize(phrase)\n  const armed = matches && !pending\n\n  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault()\n    if (!armed) return\n    onConfirm()\n  }\n\n  return (\n    <form\n      onSubmit={handleSubmit}\n      aria-busy={pending || undefined}\n      className={cn(\"flex flex-col gap-2\", className)}\n      {...props}\n    >\n      <label htmlFor={inputId} className=\"text-sm text-muted-foreground\">\n        {label ?? (\n          <>\n            Type{\" \"}\n            <code className=\"rounded bg-muted px-1 py-0.5 font-mono text-[0.9em] font-medium text-foreground\">\n              {phrase}\n            </code>{\" \"}\n            to confirm\n          </>\n        )}\n      </label>\n\n      <input\n        id={inputId}\n        type=\"text\"\n        value={typed}\n        onChange={(event) => setTyped(event.target.value)}\n        disabled={pending}\n        aria-describedby={descriptionId}\n        autoComplete=\"off\"\n        autoCorrect=\"off\"\n        autoCapitalize=\"none\"\n        spellCheck={false}\n        className=\"h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\"\n      />\n\n      <p id={descriptionId} className=\"text-xs text-muted-foreground\">\n        {description ?? (\n          <>\n            This can&apos;t be undone. {actionLabel} stays disabled until the\n            text matches.\n          </>\n        )}\n      </p>\n\n      {/*\n        Mounted at all times and empty until it has something to say: a live\n        region inserted together with its text is not reliably announced, so a\n        region that appeared only on match would swallow the very update it\n        exists for.\n      */}\n      <p role=\"status\" className=\"sr-only\">\n        {armed ? `${phrase} matches. ${actionLabel} is now enabled.` : \"\"}\n      </p>\n\n      <button\n        type=\"submit\"\n        disabled={!armed}\n        className=\"inline-flex h-9 w-full items-center justify-center rounded-md bg-destructive px-4 text-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive disabled:pointer-events-none disabled:opacity-50\"\n      >\n        {pending ? pendingLabel : actionLabel}\n      </button>\n    </form>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}