{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "doubt-button",
  "type": "registry:ui",
  "title": "DoubtButton",
  "description": "An escalating-confirmation button that makes you click through increasingly absurd, guilt-tripping prompts before the real action finally fires.",
  "files": [
    {
      "path": "components/evil-buttons/doubt-button.tsx",
      "type": "registry:ui",
      "target": "components/evil-buttons/doubt-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\ntype DoubtState = \"idle\" | \"doubting\" | \"success\";\n\nexport interface DoubtButtonProps\n  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\"> {\n  /** Label shown before any doubt has been cast. Falls back to `label`. */\n  children?: React.ReactNode;\n  /** Label shown in the idle state when no children are provided. */\n  label?: React.ReactNode;\n  /**\n   * Escalating confirmation prompts shown one per click. The final prompt, when\n   * clicked, fires `onConfirm`. The user must click `confirmations.length + 1`\n   * times total before the real action runs.\n   */\n  confirmations?: React.ReactNode[];\n  /** Label flashed once the user finally pushes through every doubt. */\n  successLabel?: React.ReactNode;\n  /** Fired only after the last confirmation is clicked. */\n  onConfirm?: () => void;\n  /** Milliseconds to stay in the success state before resetting. Set to 0 to stay. */\n  resetAfter?: number;\n}\n\nconst DEFAULT_CONFIRMATIONS: string[] = [\n  \"Are you sure?\",\n  \"Really, truly sure?\",\n  \"There is no undo. Still?\",\n  \"Think of the consequences.\",\n  \"I am obligated to ask again.\",\n  \"Last chance. Absolutely certain?\",\n];\n\nconst CheckIcon = () => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2.25\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className=\"size-3.5 shrink-0\"\n    aria-hidden\n  >\n    <path d=\"M20 6 9 17l-5-5\" />\n  </svg>\n);\n\nconst labelVariants: Variants = {\n  enter: { y: 8, opacity: 0 },\n  center: { y: 0, opacity: 1 },\n  exit: { y: -8, opacity: 0 },\n};\n\nexport const DoubtButton = React.forwardRef<\n  HTMLButtonElement,\n  DoubtButtonProps\n>(\n  (\n    {\n      children,\n      label = \"Delete everything\",\n      confirmations = DEFAULT_CONFIRMATIONS,\n      successLabel = \"Too late.\",\n      onConfirm,\n      resetAfter = 1600,\n      className,\n      disabled,\n      type = \"button\",\n      ...props\n    },\n    ref,\n  ) => {\n    const reduceMotion = useReducedMotion();\n\n    // 0 = idle; 1..steps show confirmations[index - 1]; clicking the last fires.\n    const [index, setIndex] = React.useState(0);\n    const [state, setState] = React.useState<DoubtState>(\"idle\");\n    const [shakeKey, setShakeKey] = React.useState(0);\n\n    const resetTimeoutRef = React.useRef<number | null>(null);\n\n    const steps = confirmations.length;\n\n    const clearResetTimeout = () => {\n      if (resetTimeoutRef.current !== null) {\n        window.clearTimeout(resetTimeoutRef.current);\n        resetTimeoutRef.current = null;\n      }\n    };\n\n    React.useEffect(() => () => clearResetTimeout(), []);\n\n    const handleClick = () => {\n      if (disabled || state === \"success\") return;\n\n      // No confirmations configured: behave like a normal one-shot button.\n      if (steps === 0) {\n        fire();\n        return;\n      }\n\n      if (index < steps) {\n        setIndex((i) => i + 1);\n        setState(\"doubting\");\n        setShakeKey((k) => k + 1);\n        return;\n      }\n\n      fire();\n    };\n\n    const fire = () => {\n      clearResetTimeout();\n      setState(\"success\");\n      setIndex(0);\n      onConfirm?.();\n      if (resetAfter > 0) {\n        resetTimeoutRef.current = window.setTimeout(() => {\n          setState(\"idle\");\n        }, resetAfter);\n      }\n    };\n\n    const isSuccess = state === \"success\";\n\n    // Share of doubts answered, drawn as a hairline along the bottom edge.\n    const progress = isSuccess ? 1 : steps === 0 ? 0 : index / steps;\n\n    const currentLabel: React.ReactNode = isSuccess\n      ? successLabel\n      : index === 0\n        ? (children ?? label)\n        : confirmations[index - 1];\n\n    // A stable key so AnimatePresence animates between distinct prompts.\n    const labelKey = isSuccess ? \"success\" : `step-${index}`;\n\n    return (\n      <button\n        ref={ref}\n        type={type}\n        disabled={disabled}\n        aria-live=\"polite\"\n        data-state={state}\n        onClick={handleClick}\n        className={cn(\n          \"relative inline-flex h-9 min-w-48 cursor-pointer items-center justify-center overflow-hidden rounded-[10px] px-4 text-sm font-medium text-neutral-50 outline-none select-none\",\n          // Graded dark surface matching MinimalButton: dark outer hairline, faint inner ring, top highlight, soft drop.\n          \"bg-linear-to-b from-[#353535] to-[#272727] shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.06),inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(0_0_0/0.4)]\",\n          \"transition-[scale,filter,box-shadow] duration-300 ease-[cubic-bezier(0.34,1.35,0.64,1)] hover:brightness-110\",\n          \"active:scale-[0.97] active:brightness-95 active:duration-100 active:ease-out active:shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.05),inset_0_1px_0_rgb(255_255_255/0.08),0_0_1px_rgb(0_0_0/0.2),0_1px_3px_-2px_rgb(0_0_0/0.3)] motion-reduce:active:scale-100\",\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n        {...props}\n      >\n        <motion.span\n          key={shakeKey}\n          animate={\n            reduceMotion || shakeKey === 0 || isSuccess\n              ? undefined\n              : { x: [0, -4, 4, -2, 2, 0] }\n          }\n          transition={{ duration: 0.3, ease: \"easeInOut\" }}\n          className=\"inline-flex items-center gap-1.5\"\n        >\n          {isSuccess ? <CheckIcon /> : null}\n          <span className=\"relative inline-grid\">\n            {/* Invisible widest prompt reserves width so the button never jumps. */}\n            <span\n              aria-hidden\n              className=\"invisible col-start-1 row-start-1 whitespace-nowrap\"\n            >\n              {[label, successLabel, ...confirmations].reduce<React.ReactNode>(\n                (widest, candidate) =>\n                  String(extractText(candidate)).length >\n                  String(extractText(widest)).length\n                    ? candidate\n                    : widest,\n                \"\",\n              )}\n            </span>\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              <motion.span\n                key={labelKey}\n                variants={reduceMotion ? undefined : labelVariants}\n                initial=\"enter\"\n                animate=\"center\"\n                exit=\"exit\"\n                transition={{ duration: 0.18, ease: \"easeOut\" }}\n                className=\"col-start-1 row-start-1 whitespace-nowrap text-center\"\n              >\n                {currentLabel}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n        </motion.span>\n\n        {steps > 0 && (\n          <span\n            aria-hidden\n            className=\"absolute inset-x-0 bottom-0 h-0.5 origin-left bg-current opacity-50 transition-transform duration-300 ease-out\"\n            style={{ transform: `scaleX(${progress})` }}\n          />\n        )}\n      </button>\n    );\n  },\n);\n\nfunction extractText(node: React.ReactNode): string {\n  if (node === null || node === undefined || typeof node === \"boolean\") {\n    return \"\";\n  }\n  if (typeof node === \"string\" || typeof node === \"number\") {\n    return String(node);\n  }\n  if (Array.isArray(node)) {\n    return node.map(extractText).join(\"\");\n  }\n  if (React.isValidElement<{ children?: React.ReactNode }>(node)) {\n    return extractText(node.props.children);\n  }\n  return \"\";\n}\n\nDoubtButton.displayName = \"DoubtButton\";\n\nexport default DoubtButton;\n"
    }
  ],
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ]
}
