{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-fetch",
  "title": "UseFetch",
  "description": "Abstracts the Fetch API with loading, error, and data state management.",
  "files": [
    {
      "path": "registry/hooks/use-fetch.tsx",
      "content": "'use client';\n\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n  type PropsWithChildren,\n  type ReactNode,\n} from 'react';\n\n// #region Types & Interfaces\nexport interface FetchConfig extends Omit<RequestInit, 'body' | 'signal'> {\n  baseURL?: string;\n  timeout?: number;\n  retries?: number;\n  retryDelay?: number;\n}\n\nexport interface FetchOptions<TBody = BodyInit | null>\n  extends Omit<RequestInit, 'body'> {\n  body?: TBody;\n  timeout?: number;\n  retries?: number;\n  retryDelay?: number;\n  parse?: (response: Response) => Promise<any>;\n}\n\nexport interface FetchResult<T> {\n  data: T | null;\n  error: Error | null;\n  loading: boolean;\n  refetch: () => Promise<T | null>;\n  abort: () => void;\n  aborted: boolean;\n}\n\ninterface FetchContextValue {\n  config: FetchConfig;\n  updateConfig: (newConfig: Partial<FetchConfig>) => void;\n  fetch: (url: string, options?: FetchOptions) => Promise<Response>;\n}\n\nexport interface FetchProviderProps {\n  config?: FetchConfig;\n  children: ReactNode;\n}\n\n// #endregion\n\nconst FetchContext = createContext<FetchContextValue | null>(null);\n\nexport function FetchProvider({ config = {}, children }: FetchProviderProps) {\n  const [currentConfig, setCurrentConfig] = useState<FetchConfig>(config);\n\n  const updateConfig = useCallback((newConfig: Partial<FetchConfig>) => {\n    setCurrentConfig((prev) => ({ ...prev, ...newConfig }));\n  }, []);\n\n  const enhancedFetch = useCallback(\n    async (url: string, options: FetchOptions = {}) => {\n      const {\n        timeout = currentConfig.timeout,\n        retries = currentConfig.retries ?? 0,\n        retryDelay = currentConfig.retryDelay ?? 1000,\n        ...fetchOptions\n      } = options;\n\n      // Construct full URL\n      const fullUrl =\n        currentConfig.baseURL && !url.startsWith('http')\n          ? `${currentConfig.baseURL.replace(/\\/$/, '')}/${url.replace(/^\\//, '')}`\n          : url;\n\n      // Merge headers\n      const headers = new Headers(currentConfig.headers);\n      if (fetchOptions.headers) {\n        const optionHeaders = new Headers(fetchOptions.headers);\n        optionHeaders.forEach((value, key) => headers.set(key, value));\n      }\n\n      // Create fetch options\n      const finalOptions: RequestInit = {\n        ...currentConfig,\n        ...fetchOptions,\n        headers,\n      };\n\n      // Use provided signal or create new one\n      const shouldCreateController = !fetchOptions.signal;\n      const controller = shouldCreateController ? new AbortController() : null;\n      const signal = fetchOptions.signal || controller?.signal;\n\n      // Add signal to options\n      finalOptions.signal = signal;\n\n      // Retry logic\n      let lastError: Error | null = null;\n\n      for (let attempt = 0; attempt <= retries; attempt++) {\n        try {\n          // Add timeout\n          let timeoutId: NodeJS.Timeout | null = null;\n          if (timeout && controller) {\n            timeoutId = setTimeout(() => controller.abort(), timeout);\n          }\n\n          const response = await fetch(fullUrl, finalOptions);\n\n          if (timeoutId) {\n            clearTimeout(timeoutId);\n          }\n\n          if (!response.ok) {\n            throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n          }\n\n          return response;\n        } catch (error) {\n          lastError = error as Error;\n\n          // Don't retry on abort or non-network errors\n          if (error instanceof DOMException && error.name === 'AbortError') {\n            throw error;\n          }\n\n          // If this is the last attempt, throw the error\n          if (attempt === retries) {\n            throw error;\n          }\n\n          // Wait before retry\n          await new Promise((resolve) => setTimeout(resolve, retryDelay));\n        }\n      }\n\n      throw lastError || new Error('Unknown fetch error');\n    },\n    [currentConfig],\n  );\n\n  const contextValue: FetchContextValue = {\n    config: currentConfig,\n    updateConfig,\n    fetch: enhancedFetch,\n  };\n\n  return (\n    <FetchContext.Provider value={contextValue}>\n      {children}\n    </FetchContext.Provider>\n  );\n}\n\nexport function useFetchContext(): FetchContextValue {\n  const context = useContext(FetchContext);\n\n  if (!context) {\n    throw new Error('useFetch must be used within a FetchProvider');\n  }\n\n  return context;\n}\n\nexport function useFetch<\n  T = any,\n  TBody extends BodyInit | null = BodyInit | null,\n>(\n  url: string,\n  options: FetchOptions<TBody> & { immediate?: boolean } = {},\n): FetchResult<T> {\n  const { immediate = true, parse, ...fetchOptions } = options;\n  const { fetch: contextFetch } = useFetchContext();\n\n  const [data, setData] = useState<T | null>(null);\n  const [error, setError] = useState<Error | null>(null);\n  const [loading, setLoading] = useState<boolean>(false);\n  const [aborted, setAborted] = useState<boolean>(false);\n\n  const abortController = useRef<AbortController | null>(null);\n  const isMounted = useRef(true);\n\n  useEffect(() => {\n    return () => {\n      isMounted.current = false;\n      abortController.current?.abort();\n    };\n  }, []);\n\n  const fetchData = useCallback(async (): Promise<T | null> => {\n    // Abort previous request\n    abortController.current?.abort();\n    const controller = new AbortController();\n    abortController.current = controller;\n\n    if (!isMounted.current) return null;\n\n    setLoading(true);\n    setError(null);\n    setAborted(false);\n\n    try {\n      const response = await contextFetch(url, {\n        ...fetchOptions,\n        signal: controller.signal,\n      });\n\n      if (!isMounted.current) return null;\n\n      const result = parse ? await parse(response) : await response.json();\n\n      if (isMounted.current) {\n        setData(result);\n      }\n\n      return result;\n    } catch (err) {\n      if (!isMounted.current) return null;\n\n      if (err instanceof DOMException && err.name === 'AbortError') {\n        setAborted(true);\n      } else {\n        setError(err as Error);\n        setData(null);\n      }\n      return null;\n    } finally {\n      if (isMounted.current) {\n        setLoading(false);\n      }\n    }\n  }, [url, JSON.stringify(fetchOptions), parse, contextFetch]);\n\n  useEffect(() => {\n    if (!immediate) return;\n    fetchData();\n  }, [fetchData, immediate]);\n\n  const abort = useCallback(() => {\n    abortController.current?.abort();\n    if (isMounted.current) {\n      setAborted(true);\n      setLoading(false);\n    }\n  }, []);\n\n  return {\n    data,\n    error,\n    loading,\n    refetch: fetchData,\n    abort,\n    aborted,\n  };\n}\n",
      "type": "registry:hook",
      "target": "hooks/guarahooks/use-fetch.tsx"
    }
  ],
  "categories": [
    "data-fetching"
  ],
  "type": "registry:hook"
}