{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "async-state-button",
  "type": "registry:component",
  "title": "Async State Button",
  "description": "A state-driven action button with clear loading, success, and error feedback.",
  "dependencies": [
    "motion"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/async-state-button.tsx",
      "type": "registry:component",
      "target": "components/netter/async-state-button.tsx",
      "content": "\"use client\";\n\nimport type { ButtonHTMLAttributes } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nexport type AsyncState = \"idle\" | \"loading\" | \"success\" | \"error\";\n\ntype AsyncStateButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {\n  state?: AsyncState;\n  labels?: Partial<Record<AsyncState, string>>;\n};\n\nconst defaultLabels: Record<AsyncState, string> = {\n  idle: \"Publish component\",\n  loading: \"Publishing…\",\n  success: \"Published\",\n  error: \"Try again\",\n};\n\nexport function AsyncStateButton({\n  state = \"idle\",\n  labels,\n  className = \"\",\n  disabled,\n  ...props\n}: AsyncStateButtonProps) {\n  const reduceMotion = useReducedMotion();\n  const label = labels?.[state] ?? defaultLabels[state];\n\n  return (\n    <>\n      <button\n        type=\"button\"\n        aria-busy={state === \"loading\"}\n        disabled={disabled || state === \"loading\"}\n        className={\n          \"relative inline-flex min-h-11 min-w-40 items-center justify-center overflow-hidden rounded-xl bg-zinc-950 px-5 text-sm font-semibold text-white outline-none transition-colors hover:bg-zinc-800 focus-visible:ring-2 focus-visible:ring-[#b96f4b] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-65 dark:bg-[#eee9df] dark:text-zinc-950 dark:hover:bg-[#d9d0c0] \" +\n          className\n        }\n        {...props}\n      >\n        <AnimatePresence mode=\"wait\" initial={false}>\n          <motion.span\n            key={state}\n            initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 8 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: -8 }}\n            transition={{ duration: reduceMotion ? 0.01 : 0.18, ease: [0.22, 1, 0.36, 1] }}\n            className=\"inline-flex items-center gap-2\"\n          >\n            <span\n              aria-hidden=\"true\"\n              className={\n                \"size-2 rounded-full \" +\n                (state === \"success\"\n                  ? \"bg-emerald-400\"\n                  : state === \"error\"\n                    ? \"bg-rose-400\"\n                    : state === \"loading\"\n                      ? \"animate-pulse bg-amber-300\"\n                      : \"bg-[#c98a67]\")\n              }\n            />\n            {label}\n          </motion.span>\n        </AnimatePresence>\n      </button>\n      <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n        {state === \"idle\" ? \"\" : label}\n      </span>\n    </>\n  );\n}\n"
    }
  ],
  "docs": "Control the state from your async workflow. The button announces non-idle states and disables itself while loading."
}
