{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "feature-grid",
  "type": "registry:ui",
  "title": "Feature Grid",
  "description": "Grid of small \"feature\" cards: icon + title + 1-2 line description. The shape every landing page reaches for to spell out \"what we do\" in 3-6 cells. Server component, no hooks.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "marketing",
    "pattern"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.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/feature-grid.tsx",
      "target": "components/ui/patterns/feature-grid.tsx",
      "type": "registry:ui",
      "content": "import * as React from 'react';\n\n// @interlace/feature-grid v1.1.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/feature-grid\n// What changed since: https://ds.interlace.tools/c/feature-grid#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — FeatureGrid\n *\n * Grid of small \"feature\" cards: icon + title + 1-2 line description.\n * The shape every landing page reaches for to spell out \"what we do\" in\n * 3-6 cells. Server component, no hooks.\n *\n * ## Anatomy\n *\n *   <section data-slot=\"feature-grid\" data-min-viewport=\"320\">\n *     <Container><Grid cols={3}>{features.map(...)}</Grid></Container>\n *   </section>\n *\n * | Rule | Concept                          | Where in this file                                          |\n * | ---- | -------------------------------- | ----------------------------------------------------------- |\n * | R4   | Extends native el                | `React.ComponentProps<'section'>` + FeatureGrid props       |\n * | R6   | data-slot on root                | `data-slot=\"feature-grid\"` + `data-slot=\"feature\"` per cell |\n * | R7   | className merged + ...rest       | `cn(BASE, className)` + `{...props}`                        |\n * | R10  | Composition seam (slots)         | `features` array of {icon, title, description}              |\n * | R14  | Declares min viewport            | `data-min-viewport={String(MIN_VIEWPORT)}` + exported const |\n * | R18  | Tailwind only                    | Zero inline `style`                                         |\n * | R19  | Tokens only                      | spacing + radius + token colours                            |\n * | R20  | AA contrast                      | foreground on background                                    |\n * | R25  | Server component                 | No hooks                                                    |\n * | R26  | A11y                             | `<section>` landmark; icons aria-hidden, titles as h3       |\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';\n\nexport const MIN_VIEWPORT = 320 as const;\n\ninterface FeatureItem {\n  /** Decorative icon — typically a lucide icon, marked aria-hidden. */\n  icon?: React.ReactNode;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  /** Optional CTA inside the card. */\n  action?: React.ReactNode;\n}\n\ninterface FeatureGridProps extends Omit<React.ComponentProps<'section'>, 'title'> {\n  /** Section title (renders as h2 above the grid). */\n  title?: React.ReactNode;\n  /** One-line lead under the title. */\n  lead?: React.ReactNode;\n  /** Feature cells. */\n  features?: FeatureItem[];\n  /**\n   * Grid column count at md+. Defaults to 3 (one of 2 / 3 / 4 — same as\n   * the Grid primitive's closed enum). Mobile is always 1 column.\n   */\n  cols?: 2 | 3 | 4;\n  /** When true, render a placeholder grid of card skeletons. */\n  loading?: boolean;\n  /** Skeleton count when loading. Defaults to `cols`. */\n  loadingCount?: number;\n}\n\n/**\n * Mobile-first track counts per desktop `cols`. Handed to Grid unqualified,\n * `cols` holds N narrow tracks at every width — at 375px a `cols={3}` board\n * gives each feature card ~98px and its copy overflows the viewport.\n *\n * Written out statically because Tailwind cannot scan a runtime-built\n * `sm:grid-cols-${n}`.\n */\nconst FEATURE_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 FeatureGrid({\n  title,\n  lead,\n  features = [],\n  cols = 3,\n  loading,\n  loadingCount,\n  className,\n  ...props\n}: FeatureGridProps) {\n  const skeletonCount = loadingCount ?? cols;\n  return (\n    <section\n      data-slot=\"feature-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\" align=\"center\" className=\"text-center\">\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\" className=\"max-w-prose\">\n                  {lead}\n                </Typography>\n              ) : null}\n            </Stack>\n          )}\n          {loading ? (\n            <Grid cols={cols} gap=\"md\" className={FEATURE_GRID_COLS[cols]}>\n              {Array.from({ length: skeletonCount }).map((_, i) => (\n                <Skeleton key={i} variant=\"card\" label={null} />\n              ))}\n            </Grid>\n          ) : features.length > 0 ? (\n            <Grid cols={cols} gap=\"md\" className={FEATURE_GRID_COLS[cols]}>\n              {features.map((f, i) => (\n                <article\n                  key={i}\n                  data-slot=\"feature\"\n                  className=\"border-border bg-card text-card-foreground flex flex-col gap-sm rounded-lg border p-md\"\n                >\n                  {f.icon ? (\n                    <div className=\"text-primary\" aria-hidden>\n                      {f.icon}\n                    </div>\n                  ) : null}\n                  <Typography variant=\"h4\" as=\"h3\">\n                    {f.title}\n                  </Typography>\n                  {f.description ? (\n                    <Typography variant=\"body\" tone=\"muted\">\n                      {f.description}\n                    </Typography>\n                  ) : null}\n                  {f.action ? <div className=\"mt-xs\">{f.action}</div> : null}\n                </article>\n              ))}\n            </Grid>\n          ) : null}\n        </Stack>\n      </Container>\n    </section>\n  );\n}\nFeatureGrid.displayName = 'FeatureGrid';\n\nexport { FeatureGrid };\nexport type { FeatureGridProps, FeatureItem };\n"
    }
  ],
  "meta": {
    "tier": "pattern",
    "client": false,
    "minViewport": 320,
    "loading": true,
    "version": "1.1.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/feature-grid\n\nInstalled to `components/ui/patterns/feature-grid.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/patterns/feature-grid';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/feature-grid\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
