{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "article-list-grid",
  "type": "registry:ui",
  "title": "Article List Grid",
  "description": "Grid of ArticleCards with an optional featured slot above. The pattern blog index pages reach for: one big featured card (overlay variant) above N tiles. Composes RelatedPosts-style streaming with proper loading + empty states.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "blog",
    "pattern"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/article-card.json",
    "https://ds.interlace.tools/r/cn.json",
    "https://ds.interlace.tools/r/container.json",
    "https://ds.interlace.tools/r/grid.json",
    "https://ds.interlace.tools/r/skeleton.json",
    "https://ds.interlace.tools/r/stack.json",
    "https://ds.interlace.tools/r/typography.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/patterns/article-list-grid.tsx",
      "target": "components/ui/patterns/article-list-grid.tsx",
      "type": "registry:ui",
      "content": "import * as React from 'react';\n\n// @interlace/article-list-grid v1.1.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/article-list-grid\n// What changed since: https://ds.interlace.tools/c/article-list-grid#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — ArticleListGrid\n *\n * Grid of ArticleCards with an optional featured slot above. The pattern\n * blog index pages reach for: one big featured card (overlay variant)\n * above N tiles. Composes RelatedPosts-style streaming with proper\n * loading + empty states.\n *\n * ## Anatomy\n *\n *   <section data-slot=\"article-list-grid\" data-min-viewport=\"320\">\n *     <Container>\n *       {featured && <ArticleCard variant=\"overlay\" priority />}\n *       <Grid cols={3}>{posts.map(<ArticleCard />)}</Grid>\n *     </Container>\n *   </section>\n *\n * | Rule | Concept                          | Where in this file                                          |\n * | ---- | -------------------------------- | ----------------------------------------------------------- |\n * | R4   | Extends native el                | `React.ComponentProps<'section'>` + props                   |\n * | R6   | data-slot on root                | `data-slot=\"article-list-grid\"`                             |\n * | R10  | Composition seam                 | `featured` + `posts` arrays of ArticleCard props            |\n * | R14  | Declares min viewport            | `data-min-viewport={String(MIN_VIEWPORT)}` + exported const |\n * | R18  | Tailwind only                    | Zero inline `style`                                         |\n * | R25  | Server component                 | No hooks                                                    |\n * | R26  | A11y                             | `<section>` landmark                                        |\n */\n\nimport { cn } from '@/lib/utils';\nimport { Container } from '@/components/ui/container';\nimport { Grid } from '@/components/ui/grid';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { Stack } from '@/components/ui/stack';\nimport { Typography } from '@/components/ui/typography';\nimport { ArticleCard, type ArticleCardProps } from '@/components/ui/patterns/article-card';\n\nexport const MIN_VIEWPORT = 320 as const;\n\ninterface ArticleListGridProps extends Omit<React.ComponentProps<'section'>, 'title'> {\n  title?: React.ReactNode;\n  lead?: React.ReactNode;\n  /** Optional featured article (renders as a hero card above the grid). */\n  featured?: ArticleCardProps;\n  /** Regular tiles. */\n  posts?: ArticleCardProps[];\n  cols?: 2 | 3 | 4;\n  loading?: boolean;\n  loadingCount?: number;\n  /** Custom empty state. Defaults to a small muted line. */\n  emptyState?: React.ReactNode;\n}\n\n/**\n * Mobile-first track counts per desktop `cols`. Written out statically because\n * Tailwind cannot scan a runtime-built `sm:grid-cols-${n}`.\n */\nconst ARTICLE_GRID_COLS: Record<2 | 3 | 4, string> = {\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 lg:grid-cols-4',\n};\n\nfunction ArticleListGrid({\n  title,\n  lead,\n  featured,\n  posts = [],\n  cols = 3,\n  loading,\n  loadingCount,\n  emptyState,\n  className,\n  ...props\n}: ArticleListGridProps) {\n  const skeletonCount = loadingCount ?? cols;\n  const isEmpty = !loading && !featured && posts.length === 0;\n  return (\n    <section\n      data-slot=\"article-list-grid\"\n      data-min-viewport={String(MIN_VIEWPORT)}\n      className={cn('py-xl', className)}\n      {...props}\n    >\n      <Container size=\"content\">\n        <Stack gap=\"lg\">\n          {(title || lead) && (\n            <Stack gap=\"sm\">\n              {title ? (\n                <Typography variant=\"h2\" as=\"h2\" className=\"text-balance\">\n                  {title}\n                </Typography>\n              ) : null}\n              {lead ? (\n                <Typography variant=\"long\" tone=\"muted\">\n                  {lead}\n                </Typography>\n              ) : null}\n            </Stack>\n          )}\n          {loading ? (\n            <>\n              <Skeleton variant=\"article-card\" className=\"h-72 w-full\" label={null} />\n              {/* Responsive collapse: `cols` is a DESKTOP count. Unqualified it\n                  holds 3 tracks at 375px and the truncated author name inside\n                  each card measures 0px wide — invisible. */}\n              <Grid cols={cols} gap=\"md\" className={ARTICLE_GRID_COLS[cols]}>\n                {Array.from({ length: skeletonCount }).map((_, i) => (\n                  <Skeleton key={i} variant=\"article-card\" label={null} />\n                ))}\n              </Grid>\n            </>\n          ) : isEmpty ? (\n            emptyState ?? (\n              <Typography variant=\"long\" tone=\"muted\" className=\"text-center\">\n                No articles yet.\n              </Typography>\n            )\n          ) : (\n            <>\n              {featured ? (\n                <ArticleCard {...featured} variant=\"overlay\" priority />\n              ) : null}\n              {posts.length > 0 ? (\n                <Grid cols={cols} gap=\"md\" className={ARTICLE_GRID_COLS[cols]}>\n                  {posts.map((post, i) => (\n                    <ArticleCard key={i} {...post} />\n                  ))}\n                </Grid>\n              ) : null}\n            </>\n          )}\n        </Stack>\n      </Container>\n    </section>\n  );\n}\nArticleListGrid.displayName = 'ArticleListGrid';\n\nexport { ArticleListGrid };\nexport type { ArticleListGridProps };\n"
    }
  ],
  "meta": {
    "tier": "pattern",
    "client": false,
    "minViewport": 320,
    "loading": true,
    "version": "1.1.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/article-list-grid\n\nInstalled to `components/ui/patterns/article-list-grid.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/patterns/article-list-grid';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/article-list-grid\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
