{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loading-button",
  "title": "Loading button",
  "description": "A compact solid button for one async job: its icon spins and its label swaps while the work runs, then a short go or stop status line says how it went.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/items/loading-button/loading-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotionConfig } from \"motion/react\";\nimport { RefreshCw, type LucideIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/** What an action may report back. Return nothing to mean \"it worked\". */\nexport type ActionOutcome = { ok: boolean; message?: string };\n\nexport interface LoadingButtonProps {\n  /** The work. Resolve to report success, reject or return `{ ok: false }` to report failure. */\n  onAction: () => Promise<ActionOutcome | void>;\n  /** The resting label: what pressing it will do. */\n  label: React.ReactNode;\n  /** Shown while the action runs. Defaults to \"Working…\". */\n  pendingLabel?: React.ReactNode;\n  /** Shown beside the button after success, unless the action returns its own message. */\n  successLabel?: React.ReactNode;\n  /** Shown beside the button after failure, unless the action returns its own message. */\n  errorLabel?: React.ReactNode;\n  /** The icon; it spins while the action runs. Defaults to a refresh arrow. */\n  icon?: LucideIcon;\n  disabled?: boolean;\n  className?: string;\n}\n\n/**\n * A compact solid button for one on-demand job: fetch, refresh, re-check.\n * Pressed, it spins its icon and swaps to a pending label; settled, it says\n * how it went in a short status line beside it, in the go or stop colour.\n *\n * For work the user asks for *now*. If the page needs the data to render,\n * fetch it on the page instead; a button is the override, not the plumbing.\n */\nexport function LoadingButton({\n  onAction,\n  label,\n  pendingLabel = \"Working…\",\n  successLabel = \"Done\",\n  errorLabel = \"That did not work. Try again.\",\n  icon: Icon = RefreshCw,\n  disabled = false,\n  className,\n}: LoadingButtonProps) {\n  const reduceMotion = useReducedMotionConfig();\n  const [pending, startTransition] = React.useTransition();\n  const [result, setResult] = React.useState<{\n    ok: boolean;\n    message: React.ReactNode;\n  } | null>(null);\n\n  function run() {\n    setResult(null);\n    startTransition(async () => {\n      try {\n        const outcome = await onAction();\n        const ok = outcome ? outcome.ok : true;\n        setResult({\n          ok,\n          message: outcome?.message ?? (ok ? successLabel : errorLabel),\n        });\n      } catch {\n        setResult({ ok: false, message: errorLabel });\n      }\n    });\n  }\n\n  return (\n    // The status line hangs off the button's left edge instead of sitting in\n    // the row, so the button never moves when a result appears or clears.\n    <div\n      className={cn(\"relative inline-flex items-center font-body\", className)}\n    >\n      <span className=\"pointer-events-none absolute inset-y-0 right-full flex items-center pr-2\">\n        <AnimatePresence mode=\"wait\" initial={false}>\n          {result && (\n            <motion.p\n              key={result.ok ? \"ok\" : \"error\"}\n              role=\"status\"\n              initial={reduceMotion ? { opacity: 0 } : { opacity: 0, x: 6 }}\n              animate={{ opacity: 1, x: 0 }}\n              exit={{ opacity: 0 }}\n              transition={{\n                duration: reduceMotion ? 0.1 : 0.2,\n                ease: \"easeOut\",\n              }}\n              className={cn(\n                \"text-xs font-semibold whitespace-nowrap\",\n                result.ok ? \"text-go\" : \"text-stop\",\n              )}\n            >\n              {result.message}\n            </motion.p>\n          )}\n        </AnimatePresence>\n      </span>\n\n      <button\n        type=\"button\"\n        onClick={run}\n        disabled={disabled || pending}\n        aria-busy={pending || undefined}\n        className=\"inline-flex shrink-0 cursor-pointer items-center gap-1.5 rounded-inset border border-lead bg-lead px-2.5 py-1 text-[12px] font-medium text-on-solid transition-colors hover:bg-lead-mid focus-visible:ring-2 focus-visible:ring-lead/30 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50\"\n      >\n        <Icon\n          aria-hidden=\"true\"\n          className={cn(\n            \"size-3 text-on-solid\",\n            pending && !reduceMotion && \"animate-spin\",\n          )}\n        />\n        {pending ? pendingLabel : label}\n      </button>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "kind": "widget",
    "whenToUse": "Use for an on-demand fetch, refresh or re-check the user triggers mid-task, where the outcome is worth a one-line answer. Do not use for form submission or navigation, or for data the page needs to render; fetch that on the page.",
    "origin": {
      "project": "180Connect",
      "path": "src/app/(app)/clients/[id]/grant-fetch-button.tsx"
    },
    "credits": []
  },
  "type": "registry:component"
}
