{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morph-status-button",
  "type": "registry:ui",
  "title": "MorphStatusButton",
  "description": "A single button that morphs through idle, loading, success, and error states in place, with no layout shift.",
  "files": [
    {
      "path": "components/evil-buttons/morph-status-button.tsx",
      "type": "registry:ui",
      "target": "components/evil-buttons/morph-status-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type MorphStatus = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nexport interface MorphStatusButtonProps\n  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\"> {\n  /** Idle label. Falls back to `label`. */\n  children?: React.ReactNode;\n  /** Idle label used when no children are provided. */\n  label?: React.ReactNode;\n  /** Label shown while the action is in flight. */\n  loadingLabel?: React.ReactNode;\n  /** Label shown after the action resolves. */\n  successLabel?: React.ReactNode;\n  /** Label shown after the action rejects. */\n  errorLabel?: React.ReactNode;\n  /**\n   * Click handler. If it returns a promise, the button auto-transitions\n   * through loading and into success or error based on the outcome.\n   */\n  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void | Promise<unknown>;\n  /**\n   * Controlled status. When provided, the button reflects this value and the\n   * promise-based auto-transitions are ignored.\n   */\n  status?: MorphStatus;\n  /** Milliseconds to stay in success/error before resetting. Set to 0 to stay. */\n  resetAfter?: number;\n}\n\nconst Spinner = () => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2.5\"\n    strokeLinecap=\"round\"\n    className=\"size-4 shrink-0 animate-spin\"\n    aria-hidden\n  >\n    <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n  </svg>\n);\n\nconst CheckIcon = () => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2.5\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className=\"size-4 shrink-0\"\n    aria-hidden\n  >\n    <path d=\"M20 6 9 17l-5-5\" />\n  </svg>\n);\n\nconst XIcon = () => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2.5\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className=\"size-4 shrink-0\"\n    aria-hidden\n  >\n    <path d=\"M18 6 6 18\" />\n    <path d=\"m6 6 12 12\" />\n  </svg>\n);\n\nfunction textOf(node: React.ReactNode): string {\n  if (node === null || node === undefined || typeof node === \"boolean\") {\n    return \"\";\n  }\n  if (typeof node === \"string\" || typeof node === \"number\") {\n    return String(node);\n  }\n  if (Array.isArray(node)) return node.map(textOf).join(\"\");\n  if (React.isValidElement<{ children?: React.ReactNode }>(node)) {\n    return textOf(node.props.children);\n  }\n  return \"\";\n}\n\nexport const MorphStatusButton = React.forwardRef<\n  HTMLButtonElement,\n  MorphStatusButtonProps\n>(\n  (\n    {\n      children,\n      label = \"Save changes\",\n      loadingLabel = \"Working…\",\n      successLabel = \"Done\",\n      errorLabel = \"It broke. Your fault.\",\n      onClick,\n      status: controlledStatus,\n      resetAfter = 1800,\n      className,\n      disabled,\n      ...props\n    },\n    ref,\n  ) => {\n    const reduceMotion = useReducedMotion();\n    const [internal, setInternal] = React.useState<MorphStatus>(\"idle\");\n    const resetTimeoutRef = React.useRef<number | null>(null);\n\n    const isControlled = controlledStatus !== undefined;\n    const status = isControlled ? controlledStatus : internal;\n\n    const clearReset = () => {\n      if (resetTimeoutRef.current !== null) {\n        window.clearTimeout(resetTimeoutRef.current);\n        resetTimeoutRef.current = null;\n      }\n    };\n\n    React.useEffect(() => () => clearReset(), []);\n\n    const scheduleReset = () => {\n      if (isControlled || resetAfter <= 0) return;\n      clearReset();\n      resetTimeoutRef.current = window.setTimeout(() => {\n        setInternal(\"idle\");\n      }, resetAfter);\n    };\n\n    const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {\n      if (status === \"loading\") return;\n      const result = onClick?.(e);\n\n      if (isControlled || !(result instanceof Promise)) return;\n\n      clearReset();\n      setInternal(\"loading\");\n      result\n        .then(() => {\n          setInternal(\"success\");\n          scheduleReset();\n        })\n        .catch(() => {\n          setInternal(\"error\");\n          scheduleReset();\n        });\n    };\n\n    const idleLabel = children ?? label;\n    const currentLabel: React.ReactNode =\n      status === \"loading\"\n        ? loadingLabel\n        : status === \"success\"\n          ? successLabel\n          : status === \"error\"\n            ? errorLabel\n            : idleLabel;\n\n    const icon =\n      status === \"loading\" ? (\n        <Spinner />\n      ) : status === \"success\" ? (\n        <CheckIcon />\n      ) : status === \"error\" ? (\n        <XIcon />\n      ) : null;\n\n    // Widest possible label reserves width so the button never reflows.\n    const widest = [\n      idleLabel,\n      loadingLabel,\n      successLabel,\n      errorLabel,\n    ].reduce<React.ReactNode>(\n      (widestNode, candidate) =>\n        textOf(candidate).length > textOf(widestNode).length\n          ? candidate\n          : widestNode,\n      \"\",\n    );\n\n    return (\n      <button\n        ref={ref}\n        type=\"button\"\n        onClick={handleClick}\n        disabled={disabled || status === \"loading\"}\n        aria-live=\"polite\"\n        aria-busy={status === \"loading\"}\n        data-state={status}\n        className={cn(\n          \"relative inline-flex h-9 cursor-pointer items-center justify-center overflow-hidden rounded-[10px] px-4 text-sm font-medium text-neutral-50 outline-none select-none\",\n          // Graded dark surface matching MinimalButton: dark outer hairline, faint inner ring, top highlight, soft drop.\n          \"bg-linear-to-b from-[#353535] to-[#272727] shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.06),inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(0_0_0/0.4)]\",\n          \"transition-[scale,filter,box-shadow,color] duration-300 ease-[cubic-bezier(0.34,1.35,0.64,1)] hover:brightness-110\",\n          \"active:scale-[0.97] active:brightness-95 active:duration-100 active:ease-out active:shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.05),inset_0_1px_0_rgb(255_255_255/0.08),0_0_1px_rgb(0_0_0/0.2),0_1px_3px_-2px_rgb(0_0_0/0.3)] motion-reduce:active:scale-100\",\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50\",\n          // In flight, not disabled: stay solid with a dimmed label.\n          status === \"loading\" &&\n            \"text-neutral-50/70 hover:brightness-100 disabled:opacity-100\",\n          // Error keeps the dark face; a red label and inner ring carry the blame.\n          status === \"error\" &&\n            \"text-red-300 shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(248_113_113/0.35),inset_0_1px_0_rgb(255_255_255/0.1),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(185_28_28/0.45)]\",\n          className,\n        )}\n        {...props}\n      >\n        {/* Invisible widest label reserves a stable width. */}\n        <span aria-hidden className=\"invisible inline-flex items-center gap-2\">\n          <span className=\"size-4\" />\n          {widest}\n        </span>\n\n        <span className=\"absolute inset-0 flex items-center justify-center gap-2\">\n          {icon ? (\n            <span className=\"relative inline-flex size-4 items-center justify-center\">\n              <AnimatePresence initial={false} mode=\"wait\">\n                <motion.span\n                  key={status}\n                  initial={reduceMotion ? false : { scale: 0.4, opacity: 0 }}\n                  animate={{ scale: 1, opacity: 1 }}\n                  exit={reduceMotion ? undefined : { scale: 0.4, opacity: 0 }}\n                  transition={{ type: \"spring\", stiffness: 380, damping: 22 }}\n                  className=\"inline-flex\"\n                >\n                  {icon}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n          ) : null}\n          <span className=\"relative inline-block\">\n            <AnimatePresence initial={false} mode=\"wait\">\n              <motion.span\n                key={status}\n                initial={reduceMotion ? false : { y: 8, opacity: 0 }}\n                animate={{ y: 0, opacity: 1 }}\n                exit={reduceMotion ? undefined : { y: -8, opacity: 0 }}\n                transition={{ duration: 0.16 }}\n                className=\"inline-block whitespace-nowrap\"\n              >\n                {currentLabel}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n        </span>\n      </button>\n    );\n  },\n);\n\nMorphStatusButton.displayName = \"MorphStatusButton\";\n\nexport default MorphStatusButton;\n"
    }
  ],
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ]
}
