{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "async-state",
  "title": "AsyncState",
  "description": "A declarative boundary for loading, refreshing, empty, error, offline, forbidden, and successful content.",
  "files": [
    {
      "path": "registry/async-state/async-state.tsx",
      "content": "\"use client\"\n\nimport { useRef, useState, type ReactNode } from \"react\"\n\nexport type AsyncStatus =\n  | \"idle\"\n  | \"loading\"\n  | \"refreshing\"\n  | \"empty\"\n  | \"success\"\n  | \"error\"\n  | \"offline\"\n  | \"forbidden\"\n\nexport interface AsyncStateProps {\n  status: AsyncStatus\n  children: ReactNode\n  loading?: ReactNode\n  empty?: ReactNode\n  error?: ReactNode | ((error: unknown) => ReactNode)\n  offline?: ReactNode\n  forbidden?: ReactNode\n  refreshingIndicator?: ReactNode\n  errorValue?: unknown\n  onRetry?: () => void | Promise<void>\n  preserveContentWhileRefreshing?: boolean\n  className?: string\n}\n\nconst copy = {\n  empty: {\n    label: \"No results\",\n    description: \"There is nothing to show yet.\",\n  },\n  error: {\n    label: \"Something went wrong\",\n    description: \"The request could not be completed.\",\n  },\n  offline: {\n    label: \"You are offline\",\n    description: \"Check your connection and try again.\",\n  },\n  forbidden: {\n    label: \"Access denied\",\n    description: \"You do not have permission to view this content.\",\n  },\n} as const\n\nfunction joinClassNames(...values: Array<string | undefined>) {\n  return values.filter(Boolean).join(\" \")\n}\n\nfunction LoadingView({ label = \"Loading content\" }: { label?: string }) {\n  return (\n    <div\n      className=\"bg-card text-card-foreground flex min-h-40 w-full flex-col justify-center gap-3 rounded-lg border p-6\"\n      data-slot=\"async-state-loading\"\n    >\n      <span className=\"sr-only\">{label}</span>\n      <div aria-hidden=\"true\" className=\"grid gap-3\">\n        <span className=\"bg-muted h-3 w-28 animate-pulse rounded-full motion-reduce:animate-none\" />\n        <span className=\"bg-muted h-3 w-full animate-pulse rounded-full motion-reduce:animate-none\" />\n        <span className=\"bg-muted h-3 w-4/5 animate-pulse rounded-full motion-reduce:animate-none\" />\n      </div>\n    </div>\n  )\n}\n\nfunction StateView({\n  kind,\n  onRetry,\n  retrying,\n}: {\n  kind: keyof typeof copy\n  onRetry?: () => void\n  retrying?: boolean\n}) {\n  const message = copy[kind]\n\n  return (\n    <div\n      className=\"bg-card text-card-foreground flex min-h-40 w-full flex-col items-start justify-center gap-3 rounded-lg border p-6\"\n      data-slot={`async-state-${kind}`}\n    >\n      <div className=\"grid gap-1\">\n        <p className=\"font-semibold\">{message.label}</p>\n        <p className=\"text-muted-foreground text-sm\">{message.description}</p>\n      </div>\n      {onRetry ? (\n        <button\n          className=\"bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring active:bg-primary/80 inline-flex min-h-11 items-center justify-center rounded-md px-4 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-55\"\n          disabled={retrying}\n          onClick={onRetry}\n          type=\"button\"\n        >\n          {retrying ? \"Retrying…\" : \"Try again\"}\n        </button>\n      ) : null}\n    </div>\n  )\n}\n\nfunction DefaultRefreshingIndicator() {\n  return (\n    <span className=\"bg-background text-muted-foreground inline-flex min-h-8 items-center gap-2 rounded-md border px-3 text-xs font-medium shadow-sm\">\n      <span\n        aria-hidden=\"true\"\n        className=\"bg-primary size-2 animate-pulse rounded-full motion-reduce:animate-none\"\n      />\n      Refreshing content\n    </span>\n  )\n}\n\nexport function AsyncState({\n  status,\n  children,\n  loading,\n  empty,\n  error: errorContent,\n  offline,\n  forbidden,\n  refreshingIndicator,\n  errorValue,\n  onRetry,\n  preserveContentWhileRefreshing = true,\n  className,\n}: AsyncStateProps) {\n  const [retrying, setRetrying] = useState(false)\n  const retryInFlight = useRef(false)\n\n  const handleRetry = async () => {\n    if (!onRetry || retryInFlight.current) return\n\n    retryInFlight.current = true\n    setRetrying(true)\n\n    try {\n      await onRetry()\n    } catch {\n      // The consumer owns the request error and keeps status=\"error\" on failure.\n    } finally {\n      retryInFlight.current = false\n      setRetrying(false)\n    }\n  }\n\n  const rootClassName = joinClassNames(\"relative min-w-0\", className)\n\n  if (status === \"idle\" || status === \"success\") {\n    return (\n      <div\n        className={rootClassName}\n        data-status={status}\n        data-slot=\"async-state\"\n      >\n        {children}\n      </div>\n    )\n  }\n\n  if (status === \"refreshing\") {\n    const indicator = refreshingIndicator ?? <DefaultRefreshingIndicator />\n\n    if (!preserveContentWhileRefreshing) {\n      return (\n        <div\n          aria-busy=\"true\"\n          aria-live=\"polite\"\n          className={rootClassName}\n          data-status={status}\n          data-slot=\"async-state\"\n          role=\"status\"\n        >\n          {refreshingIndicator ?? <LoadingView label=\"Refreshing content\" />}\n        </div>\n      )\n    }\n\n    return (\n      <div\n        aria-busy=\"true\"\n        className={rootClassName}\n        data-status={status}\n        data-slot=\"async-state\"\n      >\n        {children}\n        <div\n          aria-atomic=\"true\"\n          aria-live=\"polite\"\n          className=\"absolute top-3 right-3\"\n          role=\"status\"\n        >\n          {indicator}\n        </div>\n      </div>\n    )\n  }\n\n  if (status === \"loading\") {\n    return (\n      <div\n        aria-busy=\"true\"\n        aria-live=\"polite\"\n        className={rootClassName}\n        data-status={status}\n        data-slot=\"async-state\"\n        role=\"status\"\n      >\n        {loading ?? <LoadingView />}\n      </div>\n    )\n  }\n\n  const retry = onRetry ? () => void handleRetry() : undefined\n  let content: ReactNode\n\n  if (status === \"empty\") {\n    content = empty ?? <StateView kind=\"empty\" />\n  } else if (status === \"offline\") {\n    content = offline ?? (\n      <StateView kind=\"offline\" onRetry={retry} retrying={retrying} />\n    )\n  } else if (status === \"forbidden\") {\n    content = forbidden ?? <StateView kind=\"forbidden\" />\n  } else {\n    content = (typeof errorContent === \"function\"\n      ? errorContent(errorValue)\n      : errorContent) ?? (\n      <StateView kind=\"error\" onRetry={retry} retrying={retrying} />\n    )\n  }\n\n  return (\n    <div\n      aria-atomic=\"true\"\n      aria-busy={retrying || undefined}\n      aria-live={status === \"empty\" ? \"polite\" : \"assertive\"}\n      className={rootClassName}\n      data-status={status}\n      data-slot=\"async-state\"\n      role={status === \"empty\" ? \"status\" : \"alert\"}\n    >\n      {content}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/async-state.tsx"
    }
  ],
  "type": "registry:component"
}