{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-async",
  "type": "registry:hook",
  "description": "A hook that owns the async action lifecycle — loading state, success, error, and settled callbacks",
  "files": [
    {
      "type": "registry:hook",
      "target": "hooks/use-async.ts",
      "path": "src/registry/base/use-async/hooks/use-async.ts",
      "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport type AsyncStatus = \"idle\" | \"pending\" | \"success\" | \"error\";\n\n/**\n * Discriminated union for every possible state of an async operation.\n * Narrow on `status` (or the boolean flags) to get typed `data` / `error` access.\n */\nexport type AsyncState<TData, TError> =\n  | {\n      status: \"idle\";\n      data: undefined;\n      error: undefined;\n      isIdle: true;\n      isLoading: false;\n      isSuccess: false;\n      isError: false;\n    }\n  | {\n      status: \"pending\";\n      data: TData | undefined; // preserves previous data while refetching\n      error: undefined;\n      isIdle: false;\n      isLoading: true;\n      isSuccess: false;\n      isError: false;\n    }\n  | {\n      status: \"success\";\n      data: TData;\n      error: undefined;\n      isIdle: false;\n      isLoading: false;\n      isSuccess: true;\n      isError: false;\n    }\n  | {\n      status: \"error\";\n      data: undefined;\n      error: TError;\n      isIdle: false;\n      isLoading: false;\n      isSuccess: false;\n      isError: true;\n    };\n\nexport type UseAsyncOptions<TData, TError, TArgs extends unknown[]> = {\n  /** The async function to run. Receives the same args passed to `execute`. */\n  action: (...args: TArgs) => Promise<TData> | TData;\n  /** Called with the resolved value and original args on success. */\n  onSuccess?: (data: TData, args: TArgs) => void;\n  /** Called with the thrown error and original args on failure. */\n  onError?: (error: TError, args: TArgs) => void;\n  /** Called after every settled attempt regardless of outcome. */\n  onSettled?: (\n    data: TData | undefined,\n    error: TError | undefined,\n    args: TArgs,\n  ) => void;\n};\n\nexport type UseAsyncReturn<TData, TError, TArgs extends unknown[]> = AsyncState<\n  TData,\n  TError\n> & {\n  /**\n   * Run the action and await the result , even though side effect can be dispatch with onError ,\n   * but it will **Reject on failure** for imperative error handling\n   * Use when the next step depends on the  outcome (sequencing , guarded , navigation) or your own\n   * try/catch , Mirrors Tanstack `mutateAsync`\n   */\n  executeAsync: (...args: TArgs) => Promise<TData>;\n  /**\n   * Fire and Forget , **It never reject** , error handling can be listen declaratively with\n   * sideEfect with onError\n   * Use when the action is the end of the story (e.g a button that just shows success/error).\n   * Mirror Tanstack `mutate`\n   */\n  execute: (...args: TArgs) => void;\n  reset: () => void;\n};\n\nconst IDLE_STATE = {\n  status: \"idle\",\n  data: undefined,\n  error: undefined,\n  isIdle: true,\n  isLoading: false,\n  isSuccess: false,\n  isError: false,\n} as const;\n\n/**\n * Wraps an async function with loading, success, and error state.\n *\n * - Stale responses are dropped — only the most-recent `execute` call can commit\n *   state. Calling `execute` again while in-flight silently cancels the earlier one.\n * - Callbacks are always fresh; no stale-closure risk without wrapping in `useCallback`.\n * - `reset` cancels any in-flight request and returns to the `idle` state.\n */\nexport function useAsync<\n  TData = unknown,\n  TError = unknown,\n  TArgs extends unknown[] = [],\n>(\n  options: UseAsyncOptions<TData, TError, TArgs>,\n): UseAsyncReturn<TData, TError, TArgs> {\n  const [state, setState] = useState<AsyncState<TData, TError>>(IDLE_STATE);\n\n  // always fresh callbacks without forcing execute to change identity (deps array)\n  const optionsRef = useRef(options);\n  useEffect(() => {\n    optionsRef.current = options;\n  });\n\n  // latest wins , stale response will dropped\n  const requestIdRef = useRef(0);\n\n  // prevent setState after mounted\n  const mountedRef = useRef(true);\n  useEffect(() => {\n    mountedRef.current = true;\n    return () => {\n      mountedRef.current = false;\n    };\n  }, []);\n\n  const executeAsync = useCallback(async (...args: TArgs): Promise<TData> => {\n    // Tăng trước khi đọc\n    const requestId = ++requestIdRef.current;\n    const { action, onSuccess, onError, onSettled } = optionsRef.current;\n    setState((prev) => ({\n      status: \"pending\",\n      data: prev.data,\n      error: undefined,\n      isIdle: false,\n      isLoading: true,\n      isSuccess: false,\n      isError: false,\n    }));\n    try {\n      const data = await action(...args);\n      if (requestId === requestIdRef.current && mountedRef.current) {\n        // commit state only on the latest && component still mounted\n        setState({\n          status: \"success\",\n          data,\n          error: undefined,\n          isIdle: false,\n          isLoading: false,\n          isSuccess: true,\n          isError: false,\n        });\n        onSuccess?.(data, args);\n        onSettled?.(data, undefined, args);\n      }\n\n      return data;\n    } catch (err) {\n      const error = err as TError;\n      if (requestId === requestIdRef.current && mountedRef.current) {\n        setState({\n          status: \"error\",\n          data: undefined,\n          error,\n          isIdle: false,\n          isLoading: false,\n          isSuccess: false,\n          isError: true,\n        });\n        onError?.(error, args);\n        onSettled?.(undefined, error, args);\n      }\n      throw err;\n    }\n  }, []);\n\n  const execute = useCallback(async (...args: TArgs) => {\n    // Fire-and-forget: errors are already committed to state and passed to\n    // onError inside executeAsync, so we swallow the rejection here to spare\n    // callers from having to .catch. This is what makes execute \"never reject\".\n    try {\n      const data = await executeAsync(...args);\n      return data;\n    } catch {}\n  }, []);\n\n  const reset = useCallback(() => {\n    requestIdRef.current++;\n    if (mountedRef.current) {\n      setState(IDLE_STATE);\n    }\n  }, []);\n\n  return { ...state, execute, executeAsync, reset };\n}\n"
    }
  ]
}