{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "async-action",
  "title": "Async action",
  "author": "WLKR Labs",
  "description": "Promise-aware action with pending, success, failure and retry states.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/wlkr/async-action.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowUpRight, Check, X } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\n\nconst styles = `\n.wlkr-action{display:inline-grid!important;grid-template-areas:'state';height:44px;min-height:44px;max-width:100%;padding:0 12px!important}\n.wlkr-action-state{grid-area:state;display:inline-flex;align-items:center;justify-content:flex-start;gap:10px;min-height:24px;white-space:normal;text-align:left}\n.wlkr-action-state[aria-hidden=true]{visibility:hidden;pointer-events:none}\n.wlkr-action-state svg{width:16px;height:16px;flex-shrink:0}\n.wlkr-action-success{color:var(--success,#087d3d)}\n.dark .wlkr-action-success{color:var(--success,#33f282)}\n.wlkr-action-error{color:var(--destructive,#b42318)}\n.wlkr-action[aria-busy=true]{opacity:1!important}\n.wlkr-action-pending-label{position:relative;display:inline-block}\n.wlkr-action-pending-base{opacity:.72}\n.wlkr-action-pending-pixels{position:absolute;inset:0;pointer-events:none;mask-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Crect width='3' height='3' fill='white'/%3E%3Crect x='4' y='4' width='3' height='3' fill='white'/%3E%3C/svg%3E\");mask-size:8px 8px;mask-repeat:repeat}\n@keyframes wlkr-action-reconstruct{0%,100%{mask-position:0 0;opacity:.35}50%{mask-position:4px 4px;opacity:1}}\n@media(prefers-reduced-motion:no-preference){.wlkr-action-pending-pixels{animation:wlkr-action-reconstruct 2800ms ease-in-out infinite}}\n@media(prefers-reduced-motion:reduce){.wlkr-action-pending-base{opacity:1}.wlkr-action-pending-pixels{display:none}}\n\n`\n\n/** One promise at a time. All labels share a reserved slot, so feedback never moves the page. */\nexport function AsyncAction({ action, children, pendingLabel = \"Saving…\", successLabel = \"Saved\", errorLabel = \"Could not save. Try again.\", failureLabel = \"Try again\", disabled = false }: {\n  action: () => Promise<unknown>\n  children: React.ReactNode\n  pendingLabel?: string\n  successLabel?: string\n  errorLabel?: string\n  failureLabel?: string\n  disabled?: boolean\n}) {\n  const [state, setState] = React.useState<\"idle\" | \"pending\" | \"success\" | \"error\">(\"idle\")\n  const locked = React.useRef(false)\n  const mounted = React.useRef(true)\n  React.useEffect(() => { mounted.current = true; return () => { mounted.current = false } }, [])\n  async function run() {\n    if (locked.current) return\n    locked.current = true\n    setState(\"pending\")\n    try { await action(); if (mounted.current) setState(\"success\") }\n    catch { if (mounted.current) setState(\"error\") }\n    finally { locked.current = false }\n  }\n  return (\n    <span className=\"relative inline-flex max-w-full align-middle\">\n      <style>{styles}</style>\n      <Button className=\"wlkr-action\" type=\"button\" variant=\"ghost\" onClick={run} disabled={disabled || state === \"pending\"} aria-busy={state === \"pending\"}>\n        <span className=\"wlkr-action-state\" aria-hidden={state !== \"idle\"}><ArrowUpRight aria-hidden=\"true\" />{children}</span>\n        <span className=\"wlkr-action-state\" aria-hidden={state !== \"pending\"}><ArrowUpRight aria-hidden=\"true\" style={{visibility:\"hidden\"}} /><span className=\"wlkr-action-pending-label\"><span className=\"wlkr-action-pending-base\">{pendingLabel}</span><span className=\"wlkr-action-pending-pixels\" aria-hidden=\"true\">{pendingLabel}</span></span></span>\n        <span className=\"wlkr-action-state\" aria-hidden={state !== \"success\"}><Check className=\"wlkr-action-success\" aria-hidden=\"true\" />{successLabel}</span>\n        <span className=\"wlkr-action-state\" aria-hidden={state !== \"error\"}><X className=\"wlkr-action-error\" aria-hidden=\"true\" />{failureLabel}</span>\n      </Button>\n      <span role=\"status\" className=\"sr-only\">{state === \"error\" ? errorLabel : state === \"success\" ? successLabel : state === \"pending\" ? pendingLabel : \"\"}</span>\n    </span>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/wlkr/async-action.tsx"
    }
  ],
  "type": "registry:component"
}