{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bento-grid",
  "title": "Bento Grid",
  "description": "The asymmetric panel grid behind most modern feature sections: a set of cards on one grid where a few cells are deliberately two columns wide or two rows tall, so the section reads as a composition instead of a row of identical boxes. Use it for a landing page features section, a product tour, a \"why us\" grid, a homepage hero collage, a portfolio or an app-store style showcase. Common asks it answers: \"bento grid\", \"bento box layout\", \"bento cards\", \"bento section\", \"bento blocks\", \"features bento\", \"grid layout with different sized cards\", \"Apple/Linear-style feature grid\", \"masonry-ish marketing grid\", \"asymmetric card grid\". shadcn/ui ships no grid: the composition kits it does have — item, field, button-group, input-group — are flex rows that arrange the parts of one control or one row, and none of them lays tiles out across a page, so this grid is hand-rolled every time — and hand-rolling it fails in one specific way that is hard to spot: Tailwind only emits classes it can find written out in your source, so the natural `className={`col-span-${n}`}` compiles to nothing and every cell silently renders one column wide in the production build while looking correct in dev. This component keeps every span it can emit as a literal class in a lookup table, so `colSpan={2}` and `rowSpan={2}` survive the compiler. Two parts: `BentoGrid` takes `columns` (2, 3 or 4) and steps up from a single column on phones rather than fixing a track count, and `BentoGridItem` is the panel surface plus its span controls. Rows are sized minmax(11rem, auto) so equal cells line up and a tall cell is visibly twice the height; and because every layout is two columns at md and only widens at lg, each span is capped per breakpoint to the tracks that tier actually has — CSS Grid answers an over-wide span by adding an auto column, not by clamping it, and the first cell to land in that phantom column is sized by its own content. It is layout only and renders no card content of its own, so a cell can hold copy, an image, a chart or a feature-card. The grid deliberately does not use grid-auto-flow: dense — dense packing lets a later cell backfill an earlier gap, which leaves the Tab order and a screen reader reading the section in a different order than the eye sees it — and the cell is a plain div rather than a list item, so your own headings keep the document outline. No state, no effects and no \"use client\", so it renders as a server component, and it has zero npm dependencies; the surface uses shadcn tokens (card, card-foreground, border) so it follows light and dark themes. Distinct from feature-card, which is the icon/title/description content of one cell: this is the grid the cells sit on.",
  "files": [
    {
      "path": "registry/ui/bento-grid.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Tailwind only emits classes it can find written out in your source, so a span can never\n * be assembled from a prop: `col-span-${n}` compiles to nothing and the cell silently\n * renders one column wide — the single reason hand-rolled bento grids look broken in\n * production but fine in dev. Every class this component can produce is therefore a\n * literal string in one of the three maps below.\n */\nconst GRID_COLUMNS = {\n  2: \"grid-cols-1 md:grid-cols-2\",\n  3: \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\",\n  4: \"grid-cols-1 md:grid-cols-2 lg:grid-cols-4\",\n} as const\n\n/**\n * Spans step up with the grid, which is why each one lists two breakpoints. Every layout\n * above is two columns wide at `md` and only reaches three or four at `lg`, so a bare\n * `md:col-span-3` would ask for more columns than exist at that tier — and CSS Grid does\n * not clamp an over-wide span, it *adds* the missing columns to the implicit grid\n * (CSS Grid §8.5). Those extra tracks are `auto`, so the first cell that lands in one is\n * sized by its own content: measured in Chrome, a `md:col-span-3` cell in a two-column\n * grid collapsed the two real `1fr` columns from 448px to 95px and gave the phantom third\n * 403px. Capping each tier at the track count it actually has keeps the span inside the\n * explicit grid, so the cell fills the row instead of inventing a column.\n */\nconst COL_SPAN = {\n  1: \"\",\n  2: \"md:col-span-2\",\n  3: \"md:col-span-2 lg:col-span-3\",\n  4: \"md:col-span-2 lg:col-span-4\",\n} as const\n\nconst ROW_SPAN = {\n  1: \"\",\n  2: \"md:row-span-2\",\n  3: \"md:row-span-3\",\n} as const\n\ninterface BentoGridProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /**\n   * Track count at the widest breakpoint. The grid always starts at one column on\n   * phones and steps up from there, so this is the ceiling, not a fixed value.\n   */\n  columns?: keyof typeof GRID_COLUMNS\n}\n\n/**\n * The asymmetric \"bento\" grid used for feature sections and marketing pages: a set of\n * panels on a shared grid where a few cells are deliberately wider or taller than the\n * rest. Wrap the cells in `BentoGridItem` and give the ones that matter a `colSpan` or\n * `rowSpan`.\n *\n * Rows are sized `minmax(11rem, auto)` so equal-height cells line up and a `rowSpan={2}`\n * cell is visibly twice as tall; override `auto-rows-*` through `className` for a denser\n * or airier rhythm.\n *\n * It is a layout only — it renders no card content of its own, so the cells can hold\n * copy, an image, a chart or a `feature-card`. No state, no effects and no `\"use client\"`,\n * so it renders as a server component.\n */\nexport function BentoGrid({ columns = 3, className, ...props }: BentoGridProps) {\n  return (\n    <div\n      className={cn(\n        \"grid auto-rows-[minmax(11rem,auto)] gap-4\",\n        GRID_COLUMNS[columns],\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\ninterface BentoGridItemProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /**\n   * How many columns the cell covers, at the widest breakpoint. Every layout is two\n   * columns at `md`, so a cell asking for three or four spans the full row there and\n   * widens to its real span at `lg`. Keep it within the grid's own `columns`: a span\n   * larger than that has no tier where it fits, and CSS Grid answers an over-wide span by\n   * adding columns rather than by clamping it.\n   */\n  colSpan?: keyof typeof COL_SPAN\n  /** How many rows the cell covers from the `md` breakpoint up. */\n  rowSpan?: keyof typeof ROW_SPAN\n}\n\n/**\n * One cell of a `BentoGrid`: a panel surface plus the span controls that make the layout\n * asymmetric. Below `md` every cell is full width and the spans are inert, because a\n * bento layout on a phone is just a stack.\n *\n * Spans move cells around the grid but never around the document — the grid does not use\n * `grid-auto-flow: dense`, which would let a later cell backfill an earlier gap and leave\n * a screen reader and the Tab order reading the section in a different order than the eye\n * sees it.\n *\n * The cell carries no semantics of its own: it is a `div`, not a list item, so your own\n * headings keep the outline. Wrap the grid in a `<section aria-labelledby>` to name the\n * section.\n */\nexport function BentoGridItem({\n  colSpan = 1,\n  rowSpan = 1,\n  className,\n  ...props\n}: BentoGridItemProps) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col justify-between overflow-hidden rounded-xl border bg-card p-6 text-card-foreground\",\n        COL_SPAN[colSpan],\n        ROW_SPAN[rowSpan],\n        className\n      )}\n      {...props}\n    />\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}