{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "page-skeletons",
  "type": "registry:component",
  "title": "Page skeletons",
  "description": "Loading states in the page's shape: page column, header, tabs, table, cards, stats, form.",
  "registryDependencies": [
    "@wibo/utils",
    "@wibo/skeleton",
    "@wibo/page-container"
  ],
  "files": [
    {
      "path": "src/patterns/page-skeletons.tsx",
      "type": "registry:component",
      "content": "import type { ComponentProps, ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { PageContainer, type PageWidth } from \"@/components/page-container\";\n\n/*\n * Loading states that hold the shape of the page on its way: the same column\n * (`PageSkeleton`), the trail and the title row (`PageHeaderSkeleton`), then\n * the blocks the page will show (table, cards, stats, tabs, form, labelled\n * values, a list). They fade in after 50 ms, so an instant navigation does\n * not flash them. One per route segment, in `loading.tsx`, built from the\n * same parts as the page.\n */\n\nconst widths = [\"w-3/5\", \"w-2/5\", \"w-4/5\", \"w-1/2\", \"w-2/3\", \"w-1/3\"];\n\ninterface PageSkeletonProps extends ComponentProps<\"div\"> {\n  width?: PageWidth;\n  /** Announced to screen readers while the page loads. */\n  label?: string;\n}\n\n/** The page column in its loading state: busy for assistive tech, the skeleton blocks as children. */\nfunction PageSkeleton({ width = \"wide\", label = \"Loading\", className, children, ...props }: PageSkeletonProps) {\n  return (\n    <PageContainer width={width} aria-busy=\"true\" className={cn(\"animate-skeleton-in\", className)} {...props}>\n      <span role=\"status\" className=\"sr-only\">\n        {label}\n      </span>\n      {children}\n    </PageContainer>\n  );\n}\n\ninterface PageHeaderSkeletonProps {\n  /** Number of crumbs above the title; under 2 draws none, as `PageHeader` does. */\n  breadcrumbs?: number;\n  avatar?: boolean;\n  description?: boolean;\n  /** Number of action buttons on the right. */\n  actions?: number;\n}\n\n/** `PageHeader` in outline: crumbs, the title row with an optional avatar, a description line, actions. */\nfunction PageHeaderSkeleton({ breadcrumbs = 0, avatar = false, description = true, actions = 0 }: PageHeaderSkeletonProps) {\n  return (\n    <div data-slot=\"page-header-skeleton\" className=\"flex flex-col gap-5\">\n      {breadcrumbs > 1 ? (\n        <div className=\"flex h-5 items-center gap-2\">\n          {Array.from({ length: breadcrumbs }, (_, index) => (\n            <Skeleton key={index} className={cn(\"h-3\", index === breadcrumbs - 1 ? \"w-24\" : \"w-16\")} />\n          ))}\n        </div>\n      ) : null}\n      <div className=\"flex flex-wrap items-end justify-between gap-3\">\n        <div className=\"grid gap-2.5\">\n          <div className=\"flex items-center gap-3\">\n            {avatar ? <Skeleton className=\"size-8 rounded-md\" /> : null}\n            <Skeleton className=\"h-6 w-48\" />\n          </div>\n          {description ? <Skeleton className=\"h-3.5 w-72 max-w-full\" /> : null}\n        </div>\n        {actions > 0 ? (\n          <div className=\"flex gap-2\">\n            {Array.from({ length: actions }, (_, index) => (\n              <Skeleton key={index} className=\"h-8.5 w-28 rounded-control\" />\n            ))}\n          </div>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\n/** A line tab bar with `count` tabs. */\nfunction TabsSkeleton({ count = 4 }: { count?: number }) {\n  return (\n    <div className=\"flex h-9 items-center gap-5 border-b border-border\">\n      {Array.from({ length: count }, (_, index) => (\n        <Skeleton key={index} className={cn(\"h-3.5\", index === 0 ? \"w-16\" : \"w-20\")} />\n      ))}\n    </div>\n  );\n}\n\n/** A table in its card: the header row, then `rows` rows of `columns` cells. */\nfunction TableSkeleton({ rows = 6, columns = 5, className }: { rows?: number; columns?: number; className?: string }) {\n  const cells = Array.from({ length: columns }, (_, index) => index);\n  return (\n    <div className={cn(\"overflow-hidden rounded-xl border border-border bg-card shadow-card\", className)}>\n      <div className=\"flex h-8 items-center gap-6 border-b border-border px-3\">\n        {cells.map((cell) => (\n          <Skeleton key={cell} className=\"h-2.5 flex-1\" />\n        ))}\n      </div>\n      {Array.from({ length: rows }, (_, row) => (\n        <div key={row} className=\"flex h-10 items-center gap-6 border-b border-border px-3 last:border-0\">\n          {cells.map((cell) => (\n            <div key={cell} className=\"flex-1\">\n              <Skeleton className={cn(\"h-3\", widths[(row + cell) % widths.length])} />\n            </div>\n          ))}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nconst gridClass = {\n  1: \"grid-cols-1\",\n  2: \"grid-cols-1 sm:grid-cols-2\",\n  3: \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\",\n  4: \"grid-cols-1 sm:grid-cols-2 xl:grid-cols-4\",\n} as const;\n\n/** Cards in a grid: a title, `lines` lines of text, a footer row. */\nfunction CardGridSkeleton({\n  count = 3,\n  columns = 3,\n  lines = 2,\n  footer = true,\n}: {\n  count?: number;\n  columns?: 1 | 2 | 3 | 4;\n  lines?: number;\n  footer?: boolean;\n}) {\n  return (\n    <div className={cn(\"grid gap-3\", gridClass[columns])}>\n      {Array.from({ length: count }, (_, index) => (\n        <div key={index} className=\"flex flex-col gap-3 rounded-xl border border-border bg-card p-4 shadow-card\">\n          <Skeleton className=\"h-4 w-1/2\" />\n          {Array.from({ length: lines }, (_, line) => (\n            <Skeleton key={line} className={cn(\"h-3\", line === lines - 1 ? \"w-2/3\" : \"w-full\")} />\n          ))}\n          {footer ? (\n            <div className=\"mt-2 flex items-center justify-between\">\n              <Skeleton className=\"h-5 w-10\" />\n              <Skeleton className=\"h-7 w-16 rounded-control\" />\n            </div>\n          ) : null}\n        </div>\n      ))}\n    </div>\n  );\n}\n\n/** Stat cards: a label, a large number, a caption. */\nfunction StatGridSkeleton({ count = 3, columns = 3 }: { count?: number; columns?: 1 | 2 | 3 | 4 }) {\n  return (\n    <div className={cn(\"grid gap-3\", gridClass[columns])}>\n      {Array.from({ length: count }, (_, index) => (\n        <div key={index} className=\"flex flex-col gap-3 rounded-xl border border-border bg-card p-4 shadow-card\">\n          <Skeleton className=\"h-3 w-24\" />\n          <Skeleton className=\"h-7 w-16\" />\n          <Skeleton className=\"h-3 w-32\" />\n        </div>\n      ))}\n    </div>\n  );\n}\n\n/** A form in a card: `fields` labelled fields, then the submit button. */\nfunction FormSkeleton({ fields = 3, title = true }: { fields?: number; title?: boolean }) {\n  return (\n    <div className=\"flex flex-col gap-5 rounded-xl border border-border bg-card p-5 shadow-card\">\n      {title ? (\n        <div className=\"grid gap-2\">\n          <Skeleton className=\"h-4 w-40\" />\n          <Skeleton className=\"h-3 w-64 max-w-full\" />\n        </div>\n      ) : null}\n      {Array.from({ length: fields }, (_, index) => (\n        <div key={index} className=\"grid gap-2\">\n          <Skeleton className=\"h-3 w-24\" />\n          <Skeleton className=\"h-8.5 w-full rounded-control\" />\n        </div>\n      ))}\n      <Skeleton className=\"h-8.5 w-28 rounded-control\" />\n    </div>\n  );\n}\n\n/** A card of labelled values (`KeyValueList`): `rows` label and value pairs. */\nfunction KeyValueSkeleton({ rows = 5 }: { rows?: number }) {\n  return (\n    <div className=\"grid gap-3 rounded-xl border border-border bg-card p-4 shadow-card\">\n      {Array.from({ length: rows }, (_, index) => (\n        <div key={index} className=\"grid grid-cols-[8rem_1fr] items-center gap-4\">\n          <Skeleton className=\"h-3 w-20\" />\n          <Skeleton className={cn(\"h-3\", widths[index % widths.length])} />\n        </div>\n      ))}\n    </div>\n  );\n}\n\n/** A list of items in one card (`InboxList`): an icon tile, a title and a meta line, a status. */\nfunction ListSkeleton({ rows = 4 }: { rows?: number }) {\n  return (\n    <div className=\"divide-y divide-border overflow-hidden rounded-xl border border-border bg-card shadow-card\">\n      {Array.from({ length: rows }, (_, index) => (\n        <div key={index} className=\"flex items-start gap-3 px-4 py-3\">\n          <Skeleton className=\"size-7 rounded-md\" />\n          <div className=\"grid flex-1 gap-2 pt-0.5\">\n            <Skeleton className={cn(\"h-3.5\", widths[index % widths.length])} />\n            <Skeleton className=\"h-3 w-1/3\" />\n          </div>\n          <Skeleton className=\"h-5 w-16\" />\n        </div>\n      ))}\n    </div>\n  );\n}\n\n/** A plain block of a given height (a chat, a chart, an artifact frame). */\nfunction BlockSkeleton({ className, children }: { className?: string; children?: ReactNode }) {\n  return <div className={cn(\"rounded-xl border border-border bg-card p-4 shadow-card\", className)}>{children}</div>;\n}\n\nexport {\n  BlockSkeleton,\n  CardGridSkeleton,\n  FormSkeleton,\n  KeyValueSkeleton,\n  ListSkeleton,\n  PageHeaderSkeleton,\n  PageSkeleton,\n  StatGridSkeleton,\n  TableSkeleton,\n  TabsSkeleton,\n};\nexport type { PageHeaderSkeletonProps, PageSkeletonProps };\n"
    }
  ]
}
