{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "status-button",
  "title": "Status Button",
  "description": "A button that swaps between idle, loading, and success feedback for async actions.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "@persianlabsui/button",
    "@persianlabsui/spinner"
  ],
  "files": [
    {
      "path": "registry/base/ui/status-button.tsx",
      "content": "\"use client\"\n\nimport { CheckIcon } from \"lucide-react\"\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Transition,\n  type Variants,\n} from \"motion/react\"\nimport { useEffect } from \"react\"\nimport type * as React from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { useControllableState } from \"@/hooks/use-controllable-state\"\nimport { cn } from \"@/lib/utils\"\n\nexport type ButtonStatus = \"idle\" | \"loading\" | \"success\"\n\nconst DEFAULT_SUCCESS_DURATION = 1500\n\nconst swapVariants: Variants = {\n  initial: { opacity: 0, y: 8, filter: \"blur(4px)\" },\n  animate: { opacity: 1, y: 0, filter: \"blur(0px)\" },\n  exit: { opacity: 0, y: -8, filter: \"blur(4px)\" },\n}\n\nconst reducedMotionVariants: Variants = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: { opacity: 0 },\n}\n\n// A spring keeps its velocity when the status changes mid-swap; a cubic-bezier\n// would restart from zero.\nconst swapTransition: Transition = {\n  type: \"spring\",\n  duration: 0.3,\n  bounce: 0,\n}\n\n// Bounce only on the entrance, and only on movement: opacity and blur would\n// overshoot.\nconst successVariants: Variants = {\n  ...swapVariants,\n  animate: {\n    ...swapVariants.animate,\n    transition: {\n      type: \"spring\",\n      duration: 0.45,\n      bounce: 0.35,\n      opacity: swapTransition,\n      filter: swapTransition,\n    },\n  },\n}\n\nexport type StatusButtonProps = Omit<\n  React.ComponentProps<typeof Button>,\n  \"onClick\" | \"children\" | \"loading\"\n> & {\n  children: React.ReactNode\n  /**\n   * Uncontrolled mode: loading while the returned promise is pending, then\n   * success. Omit it when the form owns the submission.\n   */\n  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>\n  /** Controlled mode, e.g. derived from `useActionState`. */\n  status?: ButtonStatus\n  /** Also called for the automatic return to idle after `successDuration`. */\n  onStatusChange?: (status: ButtonStatus) => void\n  /** Shown next to the check icon, e.g. \"Submitted\" for a \"Submit\" button. */\n  successLabel?: React.ReactNode\n  successDuration?: number\n}\n\nexport function StatusButton({\n  children,\n  className,\n  onClick,\n  status: statusProp,\n  onStatusChange,\n  successLabel,\n  successDuration = DEFAULT_SUCCESS_DURATION,\n  ...props\n}: StatusButtonProps) {\n  const [status, setStatus] = useControllableState({\n    prop: statusProp,\n    defaultProp: \"idle\",\n    onChange: onStatusChange,\n    caller: \"StatusButton\",\n  })\n  const shouldReduceMotion = useReducedMotion()\n\n  useEffect(() => {\n    if (status !== \"success\") return\n\n    const timeoutId = window.setTimeout(\n      () => setStatus(\"idle\"),\n      successDuration\n    )\n    return () => window.clearTimeout(timeoutId)\n  }, [status, successDuration, setStatus])\n\n  const isBusy = status !== \"idle\"\n\n  const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {\n    if (isBusy) {\n      event.preventDefault()\n      return\n    }\n\n    if (!onClick) return\n\n    setStatus(\"loading\")\n\n    try {\n      await onClick(event)\n      setStatus(\"success\")\n    } catch (error) {\n      setStatus(\"idle\")\n      throw error\n    }\n  }\n\n  const variants = shouldReduceMotion ? reducedMotionVariants : swapVariants\n  const transition = shouldReduceMotion ? { duration: 0 } : swapTransition\n\n  const successContent = (\n    <>\n      <CheckIcon />\n      {successLabel ?? <span className=\"sr-only\">Success</span>}\n    </>\n  )\n\n  return (\n    <Button\n      data-status={status}\n      aria-busy={status === \"loading\"}\n      // Not `disabled`: busy keeps focus and the normal look, and the click\n      // handler ignores presses meanwhile.\n      aria-disabled={isBusy || undefined}\n      onClick={handleClick}\n      className={cn(\n        \"inline-grid justify-items-center *:col-start-1 *:row-start-1 *:flex *:items-center *:gap-[inherit]\",\n        className\n      )}\n      {...props}\n    >\n      {/* Invisible copies of the widest states keep the button's width fixed. */}\n      <span aria-hidden className=\"invisible\">\n        {children}\n      </span>\n      <span aria-hidden className=\"invisible\">\n        {successContent}\n      </span>\n\n      <AnimatePresence initial={false}>\n        {status === \"idle\" && (\n          <motion.span\n            key=\"idle\"\n            variants={variants}\n            initial=\"initial\"\n            animate=\"animate\"\n            exit=\"exit\"\n            transition={transition}\n          >\n            {children}\n          </motion.span>\n        )}\n\n        {status === \"loading\" && (\n          <motion.span\n            key=\"loading\"\n            variants={variants}\n            initial=\"initial\"\n            animate=\"animate\"\n            exit=\"exit\"\n            transition={transition}\n          >\n            <Spinner />\n          </motion.span>\n        )}\n\n        {status === \"success\" && (\n          <motion.span\n            key=\"success\"\n            role=\"status\"\n            variants={shouldReduceMotion ? variants : successVariants}\n            initial=\"initial\"\n            animate=\"animate\"\n            exit=\"exit\"\n            transition={transition}\n          >\n            {successContent}\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </Button>\n  )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/base/hooks/use-controllable-state.ts",
      "content": "// Thanks @radix-ui\n\nimport * as React from \"react\"\n\n// use-layout-effect.tsx\n// https://github.com/radix-ui/primitives/blob/main/packages/react/use-layout-effect/src/use-layout-effect.tsx\n\n/**\n * On the server, React emits a warning when calling `useLayoutEffect`.\n * This is because neither `useLayoutEffect` nor `useEffect` run on the server.\n * We use this safe version which suppresses the warning by replacing it with a noop on the server.\n *\n * See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect\n */\nconst useLayoutEffect = globalThis?.document ? React.useLayoutEffect : () => {}\n\n// use-controllable-state.tsx\n// https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/use-controllable-state.tsx\n\n// Prevent bundlers from trying to optimize the import\nconst useInsertionEffect: typeof useLayoutEffect =\n  (React as never)[\" useInsertionEffect \".trim().toString()] || useLayoutEffect\n\ntype ChangeHandler<T> = (state: T) => void\ntype SetStateFn<T> = React.Dispatch<React.SetStateAction<T>>\n\ninterface UseControllableStateParams<T> {\n  prop?: T | undefined\n  defaultProp: T\n  onChange?: ChangeHandler<T>\n  caller?: string\n}\n\nexport function useControllableState<T>({\n  prop,\n  defaultProp,\n  onChange = () => {},\n  caller,\n}: UseControllableStateParams<T>): [T, SetStateFn<T>] {\n  const [uncontrolledProp, setUncontrolledProp, onChangeRef] =\n    useUncontrolledState({\n      defaultProp,\n      onChange,\n    })\n  const isControlled = prop !== undefined\n  const value = isControlled ? prop : uncontrolledProp\n\n  // OK to disable conditionally calling hooks here because they will always run\n  // consistently in the same environment. Bundlers should be able to remove the\n  // code block entirely in production.\n  /* eslint-disable react-hooks/rules-of-hooks */\n  if (process.env.NODE_ENV !== \"production\") {\n    const isControlledRef = React.useRef(prop !== undefined)\n    React.useEffect(() => {\n      const wasControlled = isControlledRef.current\n      if (wasControlled !== isControlled) {\n        const from = wasControlled ? \"controlled\" : \"uncontrolled\"\n        const to = isControlled ? \"controlled\" : \"uncontrolled\"\n        console.warn(\n          `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`\n        )\n      }\n      isControlledRef.current = isControlled\n    }, [isControlled, caller])\n  }\n  /* eslint-enable react-hooks/rules-of-hooks */\n\n  const setValue = React.useCallback<SetStateFn<T>>(\n    (nextValue) => {\n      if (isControlled) {\n        const value = isFunction(nextValue) ? nextValue(prop) : nextValue\n        if (value !== prop) {\n          onChangeRef.current?.(value)\n        }\n      } else {\n        setUncontrolledProp(nextValue)\n      }\n    },\n    [isControlled, prop, setUncontrolledProp, onChangeRef]\n  )\n\n  return [value, setValue]\n}\n\nfunction useUncontrolledState<T>({\n  defaultProp,\n  onChange,\n}: Omit<UseControllableStateParams<T>, \"prop\">): [\n  Value: T,\n  setValue: React.Dispatch<React.SetStateAction<T>>,\n  OnChangeRef: React.RefObject<ChangeHandler<T> | undefined>,\n] {\n  const [value, setValue] = React.useState(defaultProp)\n  const prevValueRef = React.useRef(value)\n\n  const onChangeRef = React.useRef(onChange)\n  useInsertionEffect(() => {\n    onChangeRef.current = onChange\n  }, [onChange])\n\n  React.useEffect(() => {\n    if (prevValueRef.current !== value) {\n      onChangeRef.current?.(value)\n      prevValueRef.current = value\n    }\n  }, [value, prevValueRef])\n\n  return [value, setValue, onChangeRef]\n}\n\nfunction isFunction(value: unknown): value is (...args: never[]) => unknown {\n  return typeof value === \"function\"\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:ui"
}