{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-button-feedback",
  "title": "useButtonFeedback",
  "description": "Runs an async action through loading, success and error, skipping the spinner for fast requests and holding an error while you read it.",
  "files": [
    {
      "path": "hooks/use-button-feedback.ts",
      "content": "import * as React from \"react\"\n\ntype ButtonStatus = \"idle\" | \"loading\" | \"success\" | \"error\"\n\ntype ButtonResetAfter = number | { success?: number; error?: number }\n\ntype ButtonFeedbackOptions = {\n  resetAfter?: ButtonResetAfter\n  onStatusChange?: (status: ButtonStatus) => void\n  onError?: (error: unknown) => void\n}\n\nconst spinnerDelay = 150\nconst minimumSpinnerTime = 400\nconst resumeResetDelay = 600\nconst defaultResetAfter = { success: 2000, error: 4000 }\n\nfunction resolveResetAfter(\n  resetAfter: ButtonResetAfter | undefined,\n  outcome: \"success\" | \"error\"\n) {\n  if (typeof resetAfter === \"number\") {\n    return resetAfter\n  }\n  return resetAfter?.[outcome] ?? defaultResetAfter[outcome]\n}\n\nfunction useButtonFeedback(options: ButtonFeedbackOptions = {}) {\n  const [status, setStatus] = React.useState<ButtonStatus>(\"idle\")\n  const [error, setError] = React.useState<unknown>(undefined)\n  const optionsRef = React.useRef(options)\n  const runRef = React.useRef(0)\n  const inFlightRef = React.useRef(false)\n  const timersRef = React.useRef<ReturnType<typeof setTimeout>[]>([])\n  const holdRef = React.useRef({ hovered: false, focused: false })\n  const resetPendingRef = React.useRef(false)\n  const statusRef = React.useRef<ButtonStatus>(\"idle\")\n\n  React.useLayoutEffect(() => {\n    optionsRef.current = options\n  })\n\n  const clearTimers = React.useCallback(() => {\n    timersRef.current.forEach(clearTimeout)\n    timersRef.current = []\n  }, [])\n\n  React.useEffect(\n    () => () => {\n      runRef.current += 1\n      clearTimers()\n    },\n    [clearTimers]\n  )\n\n  const update = React.useCallback((next: ButtonStatus) => {\n    statusRef.current = next\n    setStatus(next)\n    optionsRef.current.onStatusChange?.(next)\n  }, [])\n\n  const later = React.useCallback((callback: () => void, delay: number) => {\n    const run = runRef.current\n    timersRef.current.push(\n      setTimeout(() => {\n        if (run === runRef.current) {\n          callback()\n        }\n      }, delay)\n    )\n  }, [])\n\n  const isHeld = React.useCallback(\n    () =>\n      statusRef.current === \"error\" &&\n      (holdRef.current.hovered || holdRef.current.focused),\n    []\n  )\n\n  const resume = React.useCallback(() => {\n    if (resetPendingRef.current && !isHeld()) {\n      resetPendingRef.current = false\n      later(() => update(\"idle\"), resumeResetDelay)\n    }\n  }, [isHeld, later, update])\n\n  const track = React.useCallback(\n    (action: PromiseLike<unknown> | (() => PromiseLike<unknown>)) => {\n      if (inFlightRef.current) {\n        return\n      }\n      const run = ++runRef.current\n      let spinnerShownAt: number | null = null\n\n      inFlightRef.current = true\n      resetPendingRef.current = false\n      clearTimers()\n\n      later(() => {\n        spinnerShownAt = Date.now()\n        update(\"loading\")\n      }, spinnerDelay)\n\n      const settle = (outcome: \"success\" | \"error\", reason?: unknown) => {\n        if (run !== runRef.current) {\n          return\n        }\n        clearTimers()\n        const remaining =\n          spinnerShownAt === null\n            ? 0\n            : Math.max(0, minimumSpinnerTime - (Date.now() - spinnerShownAt))\n\n        later(() => {\n          inFlightRef.current = false\n          if (outcome === \"error\") {\n            setError(reason)\n            optionsRef.current.onError?.(reason)\n          }\n          update(outcome)\n          later(\n            () => {\n              if (isHeld()) {\n                resetPendingRef.current = true\n              } else {\n                update(\"idle\")\n              }\n            },\n            resolveResetAfter(optionsRef.current.resetAfter, outcome)\n          )\n        }, remaining)\n      }\n\n      try {\n        const promise = typeof action === \"function\" ? action() : action\n        Promise.resolve(promise).then(\n          () => settle(\"success\"),\n          (reason: unknown) => settle(\"error\", reason)\n        )\n      } catch (reason) {\n        settle(\"error\", reason)\n      }\n    },\n    [clearTimers, isHeld, later, update]\n  )\n\n  const reset = React.useCallback(() => {\n    runRef.current += 1\n    clearTimers()\n    inFlightRef.current = false\n    resetPendingRef.current = false\n    update(\"idle\")\n  }, [clearTimers, update])\n\n  const handlers = React.useMemo(\n    () => ({\n      onPointerEnter: (event: React.PointerEvent<HTMLElement>) => {\n        if (event.pointerType === \"mouse\") {\n          holdRef.current.hovered = true\n        }\n      },\n      onPointerLeave: () => {\n        holdRef.current.hovered = false\n        resume()\n      },\n      onFocus: (event: React.FocusEvent<HTMLElement>) => {\n        holdRef.current.focused = event.currentTarget.matches(\":focus-visible\")\n      },\n      onBlur: () => {\n        holdRef.current.focused = false\n        resume()\n      },\n    }),\n    [resume]\n  )\n\n  return {\n    status,\n    error,\n    track,\n    reset,\n    isPending: () => inFlightRef.current,\n    buttonProps: { status, ...handlers },\n  }\n}\n\nexport { useButtonFeedback }\nexport type { ButtonFeedbackOptions, ButtonResetAfter, ButtonStatus }\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}