{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "async-action-button",
  "title": "Async Action Button",
  "description": "Promise-aware action with pending, success, error, and live status.",
  "files": [
    {
      "path": "src/components/blocks/async-action-button.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\n/*\n * SPDX-License-Identifier: MIT\n * Inspired by Amicro's asynchronous form action by Syed Subhan Uddin.\n * Upstream: https://github.com/Subhan-code/Amicro--Micro-transitions-\n * Source: src/components/forms/AnimatedFormElement.tsx\n * Audited revision: 07adc1640084940f045875e2bb1b682c90f30c3c\n * This implementation is an original accessible React/CSS adaptation.\n */\n\nexport type AsyncActionState = \"idle\" | \"pending\" | \"success\" | \"error\"\n\nexport type AsyncActionButtonProps = Omit<\n  React.ButtonHTMLAttributes<HTMLButtonElement>,\n  \"onClick\"\n> & {\n  action: () => void | Promise<unknown>\n  idleLabel?: string\n  pendingLabel?: string\n  successLabel?: string\n  errorLabel?: string\n  resetAfter?: number\n  onStateChange?: (state: AsyncActionState) => void\n}\n\nexport function AsyncActionButton({\n  action,\n  idleLabel = \"Run action\",\n  pendingLabel = \"Working\",\n  successLabel = \"Complete\",\n  errorLabel = \"Try again\",\n  resetAfter = 1800,\n  onStateChange,\n  className = \"\",\n  disabled,\n  ...props\n}: AsyncActionButtonProps) {\n  const [state, setState] = React.useState<AsyncActionState>(\"idle\")\n  const timerRef = React.useRef(0)\n  const mountedRef = React.useRef(true)\n\n  React.useEffect(() => {\n    mountedRef.current = true\n    return () => {\n      mountedRef.current = false\n      window.clearTimeout(timerRef.current)\n    }\n  }, [])\n\n  const transitionTo = React.useCallback(\n    (next: AsyncActionState) => {\n      setState(next)\n      onStateChange?.(next)\n    },\n    [onStateChange]\n  )\n\n  const labels: Record<AsyncActionState, string> = {\n    idle: idleLabel,\n    pending: pendingLabel,\n    success: successLabel,\n    error: errorLabel,\n  }\n\n  return (\n    <>\n      <button\n        type=\"button\"\n        data-async-action=\"\"\n        data-state={state}\n        aria-busy={state === \"pending\"}\n        disabled={disabled || state === \"pending\"}\n        className={`group relative inline-flex min-w-36 items-center justify-center gap-2 overflow-hidden rounded-md border px-4 py-2.5 text-sm font-medium transition-[background-color,border-color,color,transform] duration-150 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:scale-[.98] disabled:cursor-wait disabled:opacity-80 data-[state=error]:border-destructive/40 data-[state=error]:bg-destructive/10 data-[state=error]:text-destructive data-[state=idle]:border-primary data-[state=idle]:bg-primary data-[state=idle]:text-primary-foreground data-[state=pending]:border-primary data-[state=pending]:bg-primary data-[state=pending]:text-primary-foreground data-[state=success]:border-emerald-600 data-[state=success]:bg-emerald-600 data-[state=success]:text-white ${className}`}\n        onClick={async () => {\n          if (state === \"pending\") return\n          window.clearTimeout(timerRef.current)\n          transitionTo(\"pending\")\n          try {\n            await action()\n            if (!mountedRef.current) return\n            transitionTo(\"success\")\n          } catch {\n            if (!mountedRef.current) return\n            transitionTo(\"error\")\n          }\n          timerRef.current = window.setTimeout(\n            () => transitionTo(\"idle\"),\n            resetAfter\n          )\n        }}\n        {...props}\n      >\n        <span className=\"relative size-4\" aria-hidden=\"true\">\n          {state === \"pending\" ? (\n            <svg\n              data-async-spinner=\"\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              className=\"size-4\"\n            >\n              <circle\n                cx=\"12\"\n                cy=\"12\"\n                r=\"9\"\n                stroke=\"currentColor\"\n                strokeOpacity=\".28\"\n                strokeWidth=\"2.5\"\n              />\n              <path\n                d=\"M12 3a9 9 0 0 1 9 9\"\n                stroke=\"currentColor\"\n                strokeLinecap=\"round\"\n                strokeWidth=\"2.5\"\n              />\n            </svg>\n          ) : state === \"success\" ? (\n            <svg viewBox=\"0 0 24 24\" fill=\"none\" className=\"size-4\">\n              <path\n                data-async-check=\"\"\n                d=\"m5 12 4 4L19 6\"\n                stroke=\"currentColor\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth=\"2.5\"\n              />\n            </svg>\n          ) : state === \"error\" ? (\n            <svg viewBox=\"0 0 24 24\" fill=\"none\" className=\"size-4\">\n              <path\n                d=\"M12 8v5m0 3.5v.5M4.2 19h15.6L12 4 4.2 19Z\"\n                stroke=\"currentColor\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth=\"2\"\n              />\n            </svg>\n          ) : (\n            <svg viewBox=\"0 0 24 24\" fill=\"none\" className=\"size-4\">\n              <path\n                d=\"M12 3v12m0 0 4-4m-4 4-4-4M5 20h14\"\n                stroke=\"currentColor\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth=\"2\"\n              />\n            </svg>\n          )}\n        </span>\n        <span key={state} data-async-label=\"\">\n          {labels[state]}\n        </span>\n      </button>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {state === \"success\" || state === \"error\" ? labels[state] : \"\"}\n      </span>\n      <style>{`\n        @keyframes async-action-spin { to { transform: rotate(360deg); } }\n        @keyframes async-action-label { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }\n        @keyframes async-action-check { from { stroke-dashoffset: 24; } to { stroke-dashoffset: 0; } }\n        [data-async-spinner] { animation: async-action-spin .75s linear infinite; }\n        [data-async-label] { animation: async-action-label 180ms cubic-bezier(.16,1,.3,1) both; }\n        [data-async-check] { stroke-dasharray: 24; animation: async-action-check 260ms cubic-bezier(.16,1,.3,1) both; }\n        @media (prefers-reduced-motion: reduce) {\n          [data-async-spinner], [data-async-label], [data-async-check] { animation: none; }\n        }\n      `}</style>\n    </>\n  )\n}\n",
      "type": "registry:block",
      "target": "src/components/blocks/async-action-button.tsx"
    }
  ],
  "meta": {
    "collection": "motion-effects",
    "license": "MIT",
    "usage": "<AsyncActionButton\n  idleLabel=\"Save changes\"\n  action={() => fetch(\"/api/settings\", { method: \"POST\" })}\n/>",
    "source": "https://github.com/Subhan-code/Amicro--Micro-transitions-",
    "sourcePath": "src/components/forms/AnimatedFormElement.tsx",
    "sourceRevision": "07adc1640084940f045875e2bb1b682c90f30c3c"
  },
  "type": "registry:block"
}