{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "error-pages",
  "title": "Error Pages",
  "author": "@fma-ds/ds",
  "description": "Error screens on top of `empty` and `icon-stack`: `NotFound` (404), `Forbidden` (403) and `UnexpectedError`, plus the bare `ErrorPage` for other cases (maintenance, expired link).\n\nEach shows an icon, the status `code` as a small label, the title as the page's `h1`, a description and your `actions` (home, back, support).\n\n- **Layout**: `layout=\"page\"` fills the viewport; `layout=\"inline\"` fills the content area of the app's layout.\n- **`UnexpectedError`**: puts a \"Try again\" button first with `onRetry` (pending while its promise runs). With `error`, it adds a collapsible `ErrorDetails` with the message and stack and a button that copies them — leave `error` out where users shouldn't see it.\n- **In the app**: wiring to the router (error and not-found routes), reporting and automatic reloads.\n\nBuilt-in text is in English; translate it with `i18n.labels`, or override `title` and `description`.",
  "dependencies": [
    "cn@^0.5.0",
    "lucide-react@^1.61.0"
  ],
  "registryDependencies": [
    "@fma-ds/button",
    "@fma-ds/collapsible",
    "@fma-ds/copy-button",
    "@fma-ds/empty",
    "@fma-ds/icon-stack"
  ],
  "files": [
    {
      "path": "src/blocks/error-pages/error-pages.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport {\n  ChevronDownIcon,\n  LockIcon,\n  RotateCwIcon,\n  SearchXIcon,\n  TriangleAlertIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"@/core/button/button\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/core/collapsible/collapsible\";\nimport { CopyButton } from \"@/core/copy-button/copy-button\";\nimport { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia } from \"@/core/empty/empty\";\nimport { IconStack } from \"@/core/icon-stack/icon-stack\";\n\ntype ErrorPageProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n  /** `page` fills the viewport; `inline` fills the content area of the app's layout. */\n  layout?: \"page\" | \"inline\";\n  /** The glyph on the icon stack. */\n  icon?: React.ReactNode;\n  /** The status code (\"404\"), shown as a small label above the title. */\n  code?: React.ReactNode;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  /** Buttons or links below the text: go home, go back, try again. */\n  actions?: React.ReactNode;\n};\n\n/**\n * The frame of an error screen: icon, status code, title (`h1`, since it\n * replaces the page), description and actions, centered. Anything in\n * `children` (like `ErrorDetails`) goes below the actions.\n */\nfunction ErrorPage({\n  layout = \"page\",\n  icon,\n  code,\n  title,\n  description,\n  actions,\n  className,\n  children,\n  ...props\n}: ErrorPageProps) {\n  return (\n    <Empty\n      data-slot=\"error-page\"\n      data-layout={layout}\n      className={cn(\"gap-6\", layout === \"page\" ? \"min-h-svh bg-background\" : \"min-h-96\", className)}\n      {...props}\n    >\n      <EmptyHeader className=\"max-w-md\">\n        {icon && (\n          <EmptyMedia className=\"text-muted-foreground\">\n            <IconStack>{icon}</IconStack>\n          </EmptyMedia>\n        )}\n        {code && (\n          <p\n            data-slot=\"error-page-code\"\n            className=\"font-mono text-sm font-medium text-muted-foreground tabular-nums\"\n          >\n            {code}\n          </p>\n        )}\n        <h1 className=\"font-heading text-xl leading-snug font-semibold tracking-tight\">{title}</h1>\n        {description && <EmptyDescription>{description}</EmptyDescription>}\n      </EmptyHeader>\n      {(actions || children) && (\n        <EmptyContent className=\"max-w-md\">\n          {actions && (\n            <div data-slot=\"error-page-actions\" className=\"flex flex-wrap justify-center gap-2\">\n              {actions}\n            </div>\n          )}\n          {children}\n        </EmptyContent>\n      )}\n    </Empty>\n  );\n}\n\ntype ErrorDetailsLabels = {\n  details: string;\n  copy: string;\n  copied: string;\n  copyFailed: string;\n};\n\nconst defaultErrorDetailsLabels: ErrorDetailsLabels = {\n  details: \"Error details\",\n  copy: \"Copy details\",\n  copied: \"Copied\",\n  copyFailed: \"Couldn't copy\",\n};\n\n/** The error as text: the stack when there is one (it starts with the message). */\nfunction formatError(error: unknown) {\n  if (error instanceof Error) return error.stack ?? `${error.name}: ${error.message}`;\n  if (typeof error === \"string\") return error;\n  try {\n    return JSON.stringify(error, null, 2) ?? String(error);\n  } catch {\n    return String(error);\n  }\n}\n\n/**\n * Technical details behind a collapsible: the error's message and stack in\n * monospace, with a button that copies them for a support ticket.\n */\nfunction ErrorDetails({\n  error,\n  labels: labelsProp,\n  className,\n  ...props\n}: Omit<React.ComponentProps<typeof Collapsible>, \"children\"> & {\n  error: unknown;\n  labels?: Partial<ErrorDetailsLabels>;\n}) {\n  const labels = { ...defaultErrorDetailsLabels, ...labelsProp };\n  const text = formatError(error);\n\n  return (\n    <Collapsible\n      data-slot=\"error-details\"\n      className={cn(\"group/error-details w-full text-left\", className)}\n      {...props}\n    >\n      <CollapsibleTrigger className=\"mx-auto flex items-center gap-1 rounded-md px-2 py-1 text-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50\">\n        {labels.details}\n        <ChevronDownIcon\n          aria-hidden=\"true\"\n          className=\"size-4 transition-transform group-data-expanded/error-details:rotate-180\"\n        />\n      </CollapsibleTrigger>\n      <CollapsibleContent>\n        <div className=\"relative mt-2 rounded-lg border bg-muted/50\">\n          <CopyButton\n            value={text}\n            timeout={1500}\n            labels={{ copy: labels.copy, copied: labels.copied, failed: labels.copyFailed }}\n            className=\"absolute top-1.5 right-1.5\"\n          />\n          <pre className=\"max-h-64 overflow-auto p-3 pr-11 font-mono text-xs/relaxed wrap-anywhere whitespace-pre-wrap text-muted-foreground\">\n            {text}\n          </pre>\n        </div>\n      </CollapsibleContent>\n    </Collapsible>\n  );\n}\n\ntype PresetProps<Labels> = Omit<ErrorPageProps, \"title\"> & {\n  title?: React.ReactNode;\n  i18n?: { labels?: Partial<Labels> };\n};\n\ntype NotFoundLabels = { code: string; title: string; description: string };\n\nconst defaultNotFoundLabels: NotFoundLabels = {\n  code: \"404\",\n  title: \"Page not found\",\n  description: \"The page you're looking for doesn't exist or has moved.\",\n};\n\n/** The page doesn't exist. Pass the way out in `actions` (home, back). */\nfunction NotFound({ i18n, ...props }: PresetProps<NotFoundLabels>) {\n  const labels = { ...defaultNotFoundLabels, ...i18n?.labels };\n  return (\n    <ErrorPage\n      data-kind=\"not-found\"\n      icon={<SearchXIcon aria-hidden=\"true\" />}\n      code={labels.code}\n      title={labels.title}\n      description={labels.description}\n      {...props}\n    />\n  );\n}\n\ntype ForbiddenLabels = NotFoundLabels;\n\nconst defaultForbiddenLabels: ForbiddenLabels = {\n  code: \"403\",\n  title: \"Access denied\",\n  description: \"You don't have permission to view this page. Ask an administrator for access.\",\n};\n\n/** The user can't see this page. */\nfunction Forbidden({ i18n, ...props }: PresetProps<ForbiddenLabels>) {\n  const labels = { ...defaultForbiddenLabels, ...i18n?.labels };\n  return (\n    <ErrorPage\n      data-kind=\"forbidden\"\n      icon={<LockIcon aria-hidden=\"true\" />}\n      code={labels.code}\n      title={labels.title}\n      description={labels.description}\n      {...props}\n    />\n  );\n}\n\ntype UnexpectedErrorLabels = Omit<NotFoundLabels, \"code\"> &\n  ErrorDetailsLabels & {\n    retry: string;\n  };\n\nconst defaultUnexpectedErrorLabels: UnexpectedErrorLabels = {\n  title: \"Something went wrong\",\n  description: \"An unexpected error occurred. Try again; if it keeps happening, contact support.\",\n  retry: \"Try again\",\n  ...defaultErrorDetailsLabels,\n};\n\n/**\n * An unexpected failure. With `onRetry`, a \"Try again\" button comes first in\n * the actions (pending while its promise runs); with `error`, the details sit\n * in a collapsible below. Leave `error` out where users shouldn't see it.\n */\nfunction UnexpectedError({\n  error,\n  onRetry,\n  actions,\n  i18n,\n  children,\n  ...props\n}: PresetProps<UnexpectedErrorLabels> & {\n  error?: unknown;\n  onRetry?: () => void | Promise<void>;\n}) {\n  const labels = { ...defaultUnexpectedErrorLabels, ...i18n?.labels };\n  const [isPending, setIsPending] = React.useState(false);\n\n  async function retry() {\n    setIsPending(true);\n    try {\n      await onRetry?.();\n    } finally {\n      setIsPending(false);\n    }\n  }\n\n  return (\n    <ErrorPage\n      data-kind=\"unexpected-error\"\n      icon={<TriangleAlertIcon aria-hidden=\"true\" />}\n      title={labels.title}\n      description={labels.description}\n      actions={\n        (onRetry || actions) && (\n          <>\n            {onRetry && (\n              <Button isPending={isPending} onPress={retry}>\n                <RotateCwIcon\n                  data-icon=\"inline-start\"\n                  aria-hidden=\"true\"\n                  className={cn(isPending && \"animate-spin\")}\n                />\n                {labels.retry}\n              </Button>\n            )}\n            {actions}\n          </>\n        )\n      }\n      {...props}\n    >\n      {children}\n      {error !== undefined && <ErrorDetails error={error} labels={labels} />}\n    </ErrorPage>\n  );\n}\n\nexport type {\n  ErrorDetailsLabels,\n  ErrorPageProps,\n  ForbiddenLabels,\n  NotFoundLabels,\n  UnexpectedErrorLabels,\n};\nexport { ErrorDetails, ErrorPage, Forbidden, NotFound, UnexpectedError };\n",
      "type": "registry:component",
      "target": "components/fma-ds/error-pages/error-pages.tsx"
    }
  ],
  "meta": {
    "status": "experimental",
    "source": "original",
    "license": "MIT",
    "tags": [
      "block",
      "error",
      "404",
      "403",
      "not-found",
      "forbidden",
      "empty-state"
    ],
    "contentHash": "00336537e030"
  },
  "categories": [
    "blocks"
  ],
  "type": "registry:block"
}