{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "undo-button",
  "title": "Undo Button",
  "description": "An action that looks done at once and only commits after a few seconds. Until then the button is Undo.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/undo-button/undo-button.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type UndoButtonState = \"idle\" | \"pending\"\n\nexport interface UndoButtonProps extends Omit<\n  React.ButtonHTMLAttributes<HTMLButtonElement>,\n  \"children\"\n> {\n  /** Runs at once, so the host can show the action as done. */\n  onAction?: () => void\n  /** Runs once the grace window ends without an undo. */\n  onCommit: () => void\n  /** Runs when the person takes the action back. */\n  onUndo?: () => void\n  /** The grace window in milliseconds. */\n  duration?: number\n  /** The label before the action is taken. */\n  children?: React.ReactNode\n  undoLabel?: string\n  /** Says what just happened, as in \"Deleted\". */\n  message?: string\n  undoneMessage?: string\n}\n\nexport const UndoButton = React.forwardRef<HTMLButtonElement, UndoButtonProps>(\n  function UndoButton(\n    {\n      children = \"Delete\",\n      className,\n      disabled,\n      duration = 5000,\n      message = \"Deleted\",\n      onBlur,\n      onClick,\n      onCommit,\n      onAction,\n      onFocus,\n      onPointerDown,\n      onPointerEnter,\n      onPointerLeave,\n      onUndo,\n      type = \"button\",\n      undoLabel = \"Undo\",\n      undoneMessage = \"Undone\",\n      ...buttonProps\n    },\n    forwardedRef\n  ) {\n    const grace = Math.max(duration, 1000)\n    const [pending, setPending] = React.useState(false)\n    const [remaining, setRemaining] = React.useState(grace)\n    const [hovered, setHovered] = React.useState(false)\n    const [focused, setFocused] = React.useState(false)\n    const [announcement, setAnnouncement] = React.useState(\"\")\n\n    const remainingRef = React.useRef(grace)\n    const pendingRef = React.useRef(false)\n    const pointerRef = React.useRef(false)\n    const onCommitRef = React.useRef(onCommit)\n\n    React.useEffect(() => {\n      onCommitRef.current = onCommit\n    }, [onCommit])\n\n    const paused = pending && (hovered || focused)\n\n    const commit = React.useCallback(() => {\n      if (!pendingRef.current) return\n      pendingRef.current = false\n      setPending(false)\n      setAnnouncement(\"\")\n      onCommitRef.current()\n    }, [])\n\n    React.useEffect(() => {\n      if (!pending || paused) return\n\n      const deadline = performance.now() + remainingRef.current\n      let frame = requestAnimationFrame(function tick(now) {\n        const left = Math.max(deadline - now, 0)\n        remainingRef.current = left\n        setRemaining(left)\n        if (left === 0) {\n          commit()\n          return\n        }\n        frame = requestAnimationFrame(tick)\n      })\n\n      return () => {\n        cancelAnimationFrame(frame)\n        remainingRef.current = Math.max(deadline - performance.now(), 0)\n      }\n    }, [commit, paused, pending])\n\n    // Leaving the page or the list does not take the action back: the person\n    // was told it happened, so it happens.\n    React.useEffect(() => () => commit(), [commit])\n\n    const seconds = Math.ceil(remaining / 1000)\n\n    return (\n      <>\n        <button\n          {...buttonProps}\n          ref={forwardedRef}\n          type={type}\n          disabled={disabled}\n          data-slot=\"undo-button\"\n          data-state={pending ? \"pending\" : \"idle\"}\n          data-paused={paused || undefined}\n          onClick={(event) => {\n            onClick?.(event)\n            if (event.defaultPrevented) return\n\n            if (pendingRef.current) {\n              pendingRef.current = false\n              setPending(false)\n              setAnnouncement(undoneMessage)\n              onUndo?.()\n              return\n            }\n\n            remainingRef.current = grace\n            pendingRef.current = true\n            setRemaining(grace)\n            setPending(true)\n            onAction?.()\n            setAnnouncement(\n              `${message}. ${undoLabel} available for ${Math.ceil(grace / 1000)} seconds`\n            )\n          }}\n          onPointerDown={(event) => {\n            onPointerDown?.(event)\n            pointerRef.current = true\n          }}\n          onFocus={(event) => {\n            onFocus?.(event)\n            // Focus that follows a click is a side effect of the pointer, not\n            // someone reading the button, so it must not hold the clock.\n            setFocused(!pointerRef.current)\n            pointerRef.current = false\n          }}\n          onBlur={(event) => {\n            onBlur?.(event)\n            setFocused(false)\n          }}\n          onPointerEnter={(event) => {\n            onPointerEnter?.(event)\n            if (event.pointerType === \"mouse\") setHovered(true)\n          }}\n          onPointerLeave={(event) => {\n            onPointerLeave?.(event)\n            setHovered(false)\n          }}\n          className={cn(\n            \"border-border bg-background text-foreground hover:bg-muted focus-visible:ring-ring/50 relative isolate inline-flex min-h-10 min-w-28 items-center justify-center gap-2 overflow-hidden rounded-[var(--radius)] border px-4 text-sm font-medium transition-colors duration-150 outline-none select-none focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none\",\n            className\n          )}\n        >\n          {pending ? (\n            <>\n              <span\n                aria-hidden=\"true\"\n                className=\"bg-foreground/10 absolute inset-y-0 left-0 -z-10 w-full origin-left motion-reduce:hidden\"\n                style={{ transform: `scaleX(${remaining / grace})` }}\n              />\n              <span>{undoLabel}</span>\n              <span\n                aria-hidden=\"true\"\n                className=\"text-muted-foreground min-w-[2ch] text-xs tabular-nums\"\n              >\n                {seconds}s\n              </span>\n            </>\n          ) : (\n            children\n          )}\n        </button>\n        <span aria-live=\"polite\" className=\"sr-only\">\n          {announcement}\n        </span>\n      </>\n    )\n  }\n)\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "button",
    "control",
    "feedback",
    "form"
  ],
  "type": "registry:ui"
}