{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confirm-dialog",
  "title": "ConfirmDialog",
  "description": "Destructive confirmation modal armed by press-and-hold, with keyboard hold support.",
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/neon-tokens.json",
    "https://ui.neon.com/r/button.json",
    "https://ui.neon.com/r/dialog.json"
  ],
  "files": [
    {
      "path": "src/components/confirm-dialog/confirm-dialog.tsx",
      "content": "\"use client\";\n\nimport type { CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ConfirmDialogProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  /** Runs after the hold completes; the dialog closes itself. */\n  onConfirm: () => void;\n  /** One line naming the action, e.g. \"Restore this checkpoint?\". */\n  title: string;\n  /** What happens and what it costs — say it plainly. */\n  description?: ReactNode;\n  /** Label inside the hold action. */\n  confirmLabel?: string;\n  cancelLabel?: string;\n  /** How long the hold takes to arm, in milliseconds. */\n  holdMs?: number;\n  /**\n   * The hold trembles as it approaches commitment — barely a shiver\n   * at the start, unmistakable by the end. Off under reduced motion.\n   */\n  shake?: boolean;\n}\n\n/* ─────────────────────────────────────────────────────────\n * HOLD-TO-CONFIRM STORYBOARD\n *\n * A destructive action shouldn't be one twitch away. The\n * dialog asks; the hold answers.\n *\n *  open      overlay fades, the panel rises 8px\n *  rest      the action wears destructive as a border and\n *            text — armed, not fired\n *  hold      press and hold: a destructive fill sweeps\n *            left to right for exactly holdMs (linear —\n *            progress, not easing). The fill carries its\n *            own destructive-foreground copy of the label,\n *            revealed by the same clip-path, so the sweep\n *            edge crosses the letterforms — white behind\n *            it, destructive ahead of it, never a flip.\n *            With shake on, the button trembles harder as\n *            the fill closes in — amplitude eases from 0\n *            to 2.5px over the hold (ease-in: dread builds\n *            late), mixing axes so it reads as strain\n *  release   let go early and the fill springs back\n *            (180ms ease-out) — no harm done\n *  arm       the fill lands, onConfirm fires once, the\n *            dialog closes\n *  keyboard  holding Space or Enter works the same way;\n *            key repeat is ignored\n *\n * The clip-path label split was suggested by Gurbinder\n * (x.com/legionsdev).\n * ───────────────────────────────────────────────────────── */\nconst DEFAULT_HOLD_MS = 1200;\nconst RELEASE_MS = 180;\n/** Peak tremble at the moment the hold arms — a shiver, not a quake. */\nconst SHAKE_MAX = \"0.75px\";\n/**\n * The amplitude's ramp: flat for most of the hold, then it surges —\n * gradually, then suddenly.\n */\nconst SHAKE_EASE = \"cubic-bezier(0.8, 0, 1, 1)\";\n\nexport const ConfirmDialog = ({\n  cancelLabel = \"Cancel\",\n  confirmLabel = \"Hold to confirm\",\n  description,\n  holdMs = DEFAULT_HOLD_MS,\n  onConfirm,\n  onOpenChange,\n  open,\n  shake = true,\n  title,\n}: ConfirmDialogProps) => {\n  const [holding, setHolding] = useState(false);\n  const timerRef = useRef(0);\n\n  const cancelHold = () => {\n    window.clearTimeout(timerRef.current);\n    setHolding(false);\n  };\n\n  const startHold = () => {\n    setHolding(true);\n    timerRef.current = window.setTimeout(() => {\n      setHolding(false);\n      onConfirm();\n      onOpenChange(false);\n    }, holdMs);\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n    if (event.repeat || !(event.key === \"Enter\" || event.key === \" \")) {\n      return;\n    }\n\n    event.preventDefault();\n\n    if (!holding) {\n      startHold();\n    }\n  };\n\n  /** Any close path cancels an in-flight hold first. */\n  const handleOpenChange = (next: boolean) => {\n    if (!next) {\n      cancelHold();\n    }\n\n    onOpenChange(next);\n  };\n\n  // Leave no timer behind on unmount.\n  useEffect(() => () => window.clearTimeout(timerRef.current), []);\n\n  return (\n    <Dialog onOpenChange={handleOpenChange} open={open}>\n      <DialogContent data-slot=\"confirm-dialog\">\n        <DialogHeader>\n          <DialogTitle>{title}</DialogTitle>\n          {description ? (\n            <DialogDescription>{description}</DialogDescription>\n          ) : null}\n        </DialogHeader>\n        <DialogFooter>\n          <Button\n            className=\"active:scale-[0.98]\"\n            onClick={() => handleOpenChange(false)}\n            variant=\"ghost\"\n          >\n            {cancelLabel}\n          </Button>\n          <Button\n            className={cn(\n              \"relative select-none overflow-hidden rounded-md border border-destructive/50 bg-destructive/10 text-destructive hover:bg-destructive/15 hover:text-destructive focus-visible:border-destructive/60 focus-visible:ring-destructive/25 dark:focus-visible:ring-destructive/40\",\n              shake && holding && \"neon-hold-shake\"\n            )}\n            data-holding={holding || undefined}\n            data-slot=\"confirm-dialog-hold\"\n            style={\n              shake\n                ? ({\n                    \"--neon-shake-amp\": holding ? SHAKE_MAX : \"0px\",\n                    transition: `--neon-shake-amp ${\n                      holding ? holdMs : RELEASE_MS\n                    }ms ${holding ? SHAKE_EASE : \"ease-out\"}`,\n                  } as CSSProperties)\n                : undefined\n            }\n            onKeyDown={handleKeyDown}\n            onKeyUp={cancelHold}\n            onPointerCancel={cancelHold}\n            onPointerDown={startHold}\n            onPointerLeave={cancelHold}\n            onPointerUp={cancelHold}\n            variant=\"ghost\"\n          >\n            <span className=\"relative\">{confirmLabel}</span>\n            {/* The progress fill and its own white copy of the label,\n                revealed together by one clip-path — the sweep edge\n                crosses the letterforms instead of flipping the text. */}\n            <span\n              aria-hidden=\"true\"\n              className=\"absolute inset-0 flex items-center justify-center bg-destructive text-destructive-foreground\"\n              data-slot=\"confirm-dialog-hold-fill\"\n              style={{\n                clipPath: holding ? \"inset(0 0% 0 0)\" : \"inset(0 100% 0 0)\",\n                transition: `clip-path ${holding ? holdMs : RELEASE_MS}ms ${\n                  holding ? \"linear\" : \"ease-out\"\n                }`,\n              }}\n            >\n              {confirmLabel}\n            </span>\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}