{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bento-grid",
  "title": "Bento Grid",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A responsive bento layout of feature cards that span columns and rows. Each BentoCard has a background visual slot, icon, title and description, and a link that slides up on hover or focus.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/media.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/bento-grid.tsx",
      "content": "// Ballmac UI: Bento Grid. https://ui.ballmac.com/components/bento-grid\nimport * as React from \"react\"\nimport { ArrowRight } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Media, isMediaImage, type MediaSource } from \"@/components/ballmac/media\"\n\ntype BentoGridProps = React.ComponentProps<\"div\"> & {\n  /** Columns once the grid is wider than 36rem; narrower grids stack in one column. */\n  columns?: 2 | 3 | 4\n  /** Minimum height of one row, any CSS length. */\n  rowHeight?: string\n}\n\nconst COLUMNS = {\n  2: \"@xl/bento:grid-cols-2\",\n  3: \"@xl/bento:grid-cols-3\",\n  4: \"@xl/bento:grid-cols-2 @4xl/bento:grid-cols-4\",\n} as const\n\nfunction BentoGrid({ columns = 3, rowHeight = \"13rem\", className, style, children, ...props }: BentoGridProps) {\n  return (\n    <div data-slot=\"bento-grid\" className={cn(\"@container/bento w-full\", className)} style={style} {...props}>\n      <div\n        className={cn(\"grid grid-cols-1 gap-3 auto-rows-[minmax(var(--bento-row),auto)]\", COLUMNS[columns])}\n        style={{ \"--bento-row\": rowHeight } as React.CSSProperties}\n      >\n        {children}\n      </div>\n    </div>\n  )\n}\n\ntype BentoCardProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n  /** Columns the card spans in the wide layout. */\n  colSpan?: 1 | 2 | 3 | 4\n  /** Rows the card spans in the wide layout. */\n  rowSpan?: 1 | 2\n  /** Decorative visual that fills the card behind the text; it fades out toward the text. */\n  background?: MediaSource\n  /** Small icon shown above the title. */\n  icon?: React.ReactNode\n  /** Card title. */\n  title: React.ReactNode\n  /** One or two lines under the title. */\n  description?: React.ReactNode\n  /** Makes the whole card a link to this URL. */\n  href?: string\n  /** Label of the link revealed on hover and focus. */\n  cta?: string\n}\n\nconst COL_SPAN = { 1: \"\", 2: \"@xl/bento:col-span-2\", 3: \"@xl/bento:col-span-3\", 4: \"@xl/bento:col-span-2 @4xl/bento:col-span-4\" } as const\nconst ROW_SPAN = { 1: \"\", 2: \"@xl/bento:row-span-2\" } as const\n\nfunction BentoCard({\n  colSpan = 1,\n  rowSpan = 1,\n  background,\n  icon,\n  title,\n  description,\n  href,\n  cta = \"Learn more\",\n  className,\n  children,\n  ...props\n}: BentoCardProps) {\n  return (\n    <div\n      data-slot=\"bento-card\"\n      className={cn(\n        \"group/bento relative isolate flex min-h-(--bento-row) flex-col justify-end overflow-hidden rounded-xl border bg-card text-card-foreground shadow-[0_1px_2px_rgb(0_0_0/0.04)] transition-[border-color,box-shadow] duration-200 hover:border-foreground/15 has-[a:focus-visible]:ring-[3px] has-[a:focus-visible]:ring-ring/50 dark:shadow-none\",\n        COL_SPAN[colSpan],\n        ROW_SPAN[rowSpan],\n        className\n      )}\n      {...props}\n    >\n      {background && (\n        <div\n          aria-hidden=\"true\"\n          inert\n          data-slot=\"bento-card-background\"\n          className=\"pointer-events-none absolute inset-0 -z-10 [mask-image:linear-gradient(to_bottom,black_35%,transparent_72%)] transition-transform duration-500 ease-(--bm-ease-out) group-hover/bento:scale-[1.03] motion-reduce:transition-none motion-reduce:group-hover/bento:scale-100\"\n        >\n          {isMediaImage(background) ? <Media media={background} alt=\"\" fill className=\"absolute inset-0\" /> : background}\n        </div>\n      )}\n      {children}\n      <div\n        data-slot=\"bento-card-content\"\n        className={cn(\n          \"relative p-5 transition-transform duration-300 ease-(--bm-ease-out) motion-reduce:transition-none\",\n          // Slides up to make room for the link, only where hover exists; touch screens always show it.\n          href && \"[@media(hover:hover)]:translate-y-9 [@media(hover:hover)]:group-hover/bento:translate-y-0 [@media(hover:hover)]:group-has-[a:focus-visible]/bento:translate-y-0\"\n        )}\n      >\n        {icon && (\n          <div className=\"mb-3 flex size-9 items-center justify-center rounded-lg border bg-background/80 text-foreground shadow-xs backdrop-blur [&_svg]:size-4\">\n            {icon}\n          </div>\n        )}\n        <h3 className=\"text-base font-semibold tracking-tight\">{title}</h3>\n        {description && <p className=\"mt-1 max-w-sm text-sm text-muted-foreground\">{description}</p>}\n        {href && (\n          <a\n            href={href}\n            data-slot=\"bento-card-link\"\n            className=\"mt-3 inline-flex items-center gap-1 text-sm font-medium outline-none transition-opacity duration-300 after:absolute after:inset-0 after:content-[''] [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover/bento:opacity-100 [@media(hover:hover)]:focus-visible:opacity-100\"\n          >\n            {cta}\n            <ArrowRight aria-hidden=\"true\" className=\"size-3.5 transition-transform duration-200 group-hover/bento:translate-x-0.5 rtl:rotate-180 rtl:group-hover/bento:-translate-x-0.5\" />\n          </a>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { BentoGrid, BentoCard, type BentoGridProps, type BentoCardProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/bento-grid.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "bento",
      "grid",
      "features",
      "landing",
      "cards",
      "layout"
    ],
    "version": "1.0.0",
    "updated": "2026-09-29",
    "ai": {
      "summary": "<BentoGrid columns={3}> with <BentoCard colSpan rowSpan icon title description href background />. The grid responds to its own width (container queries): one column below 36rem. The background slot takes any decorative node, such as animated-grid, marquee or number-ticker visuals.",
      "whenToUse": [
        "Landing-page feature sections with 4–7 features of different weight",
        "Product overviews where each tile shows a small live visual",
        "Dashboard home pages with shortcuts"
      ],
      "whenNotToUse": [
        "Uniform lists of equal items (use a plain grid of cards)",
        "Long text content; cards hold one or two lines"
      ],
      "composesWith": [
        "animated-grid",
        "marquee",
        "number-ticker",
        "border-beam"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Focuses the card's link; the whole card is its click target and shows a focus ring"
        }
      ],
      "customization": [
        "BentoGrid: columns 2 | 3 | 4 (default 3), rowHeight (default 13rem)",
        "BentoCard: colSpan 1–4, rowSpan 1–2, href + cta (default \"Learn more\")",
        "background is aria-hidden and masked to fade toward the text",
        "On touch screens the link is always visible"
      ]
    },
    "examples": [
      "bento-grid-demo",
      "bento-grid-features"
    ],
    "url": "https://ui.ballmac.com/components/bento-grid"
  },
  "categories": [
    "layout"
  ],
  "type": "registry:ui"
}