{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "masonry-grid",
  "title": "Masonry Grid",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A gap-free multi-column layout for tiles of different heights, built on CSS columns so it never shifts on load, with responsive column counts and optional reveal.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/motion-presets.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/masonry-grid.tsx",
      "content": "// Ballmac UI: Masonry Grid. https://ui.ballmac.com/components/masonry-grid\n\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { ease } from \"@/lib/ballmac/motion\";\nimport { cn } from \"@/lib/utils\";\n\ntype ColumnCount = 1 | 2 | 3 | 4 | 5 | 6;\ntype MasonryColumns = ColumnCount | { base?: ColumnCount; sm?: ColumnCount; md?: ColumnCount; lg?: ColumnCount; xl?: ColumnCount };\n\nconst BASE: Record<ColumnCount, string> = { 1: \"columns-1\", 2: \"columns-2\", 3: \"columns-3\", 4: \"columns-4\", 5: \"columns-5\", 6: \"columns-6\" };\nconst SM: Record<ColumnCount, string> = { 1: \"sm:columns-1\", 2: \"sm:columns-2\", 3: \"sm:columns-3\", 4: \"sm:columns-4\", 5: \"sm:columns-5\", 6: \"sm:columns-6\" };\nconst MD: Record<ColumnCount, string> = { 1: \"md:columns-1\", 2: \"md:columns-2\", 3: \"md:columns-3\", 4: \"md:columns-4\", 5: \"md:columns-5\", 6: \"md:columns-6\" };\nconst LG: Record<ColumnCount, string> = { 1: \"lg:columns-1\", 2: \"lg:columns-2\", 3: \"lg:columns-3\", 4: \"lg:columns-4\", 5: \"lg:columns-5\", 6: \"lg:columns-6\" };\nconst XL: Record<ColumnCount, string> = { 1: \"xl:columns-1\", 2: \"xl:columns-2\", 3: \"xl:columns-3\", 4: \"xl:columns-4\", 5: \"xl:columns-5\", 6: \"xl:columns-6\" };\n\nconst GAPS = {\n  sm: { gap: \"gap-3\", item: \"mb-3\" },\n  md: { gap: \"gap-4\", item: \"mb-4\" },\n  lg: { gap: \"gap-6\", item: \"mb-6\" },\n} as const;\n\ntype MasonryGridProps = React.ComponentProps<\"div\"> & {\n  /** Number of columns, or a count per breakpoint: `{ base: 1, sm: 2, lg: 4 }`. */\n  columns?: MasonryColumns;\n  /** Space between items. */\n  gap?: keyof typeof GAPS;\n  /** Fade items up as they scroll into view. Off under reduced motion. */\n  reveal?: boolean;\n};\n\nconst GapContext = React.createContext<keyof typeof GAPS>(\"md\");\nconst RevealContext = React.createContext(false);\n\nfunction columnClasses(columns: MasonryColumns) {\n  if (typeof columns === \"number\") return BASE[columns];\n  return [\n    columns.base && BASE[columns.base],\n    columns.sm && SM[columns.sm],\n    columns.md && MD[columns.md],\n    columns.lg && LG[columns.lg],\n    columns.xl && XL[columns.xl],\n  ];\n}\n\n/**\n * A multi-column layout where items of different heights pack without gaps. Built on CSS columns, so it renders the\n * same on the server and in the browser, never shifts on load, and keeps DOM order equal to reading order\n * (down the first column, then the next).\n */\nfunction MasonryGrid({\n  columns = { base: 1, sm: 2, lg: 3 },\n  gap = \"md\",\n  reveal = false,\n  className,\n  ...props\n}: MasonryGridProps) {\n  return (\n    <GapContext.Provider value={gap}>\n      <RevealContext.Provider value={reveal}>\n        <div\n          data-slot=\"masonry-grid\"\n          className={cn(columnClasses(columns), GAPS[gap].gap, className)}\n          {...props}\n        />\n      </RevealContext.Provider>\n    </GapContext.Provider>\n  );\n}\n\ntype MasonryItemProps = Omit<React.ComponentProps<\"div\">, \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\">;\n\n/** One tile. It never splits across columns. */\nfunction MasonryItem({ className, children, ...props }: MasonryItemProps) {\n  const gap = React.useContext(GapContext);\n  const reveal = React.useContext(RevealContext);\n  const reduce = useReducedMotion();\n  const classes = cn(\"break-inside-avoid\", GAPS[gap].item, className);\n  if (!reveal || reduce) {\n    return (\n      <div data-slot=\"masonry-item\" className={classes} {...props}>\n        {children}\n      </div>\n    );\n  }\n  return (\n    <motion.div\n      data-slot=\"masonry-item\"\n      className={classes}\n      initial={{ opacity: 0, y: 16 }}\n      whileInView={{ opacity: 1, y: 0 }}\n      viewport={{ once: true, margin: \"0px 0px -8% 0px\" }}\n      transition={{ duration: 0.5, ease: ease.out }}\n      {...props}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport { MasonryGrid, MasonryItem, type MasonryGridProps, type MasonryItemProps, type MasonryColumns };\n",
      "type": "registry:ui",
      "target": "@components/ballmac/masonry-grid.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "masonry",
      "grid",
      "gallery",
      "columns"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "<MasonryGrid columns={{base:2, lg:4}} gap reveal><MasonryItem/>…</MasonryGrid>. DOM order is reading order: down the first column, then the next.",
      "whenToUse": [
        "Galleries, portfolios and boards",
        "Testimonials or notes of uneven length"
      ],
      "whenNotToUse": [
        "Rows that must line up across columns; use CSS grid",
        "Sortable boards; use kanban-board"
      ],
      "composesWith": [
        "card",
        "bento-grid"
      ],
      "a11y": [
        {
          "keys": "Tab order",
          "action": "Follows DOM order, which matches the visual column flow"
        },
        {
          "keys": "Reduced motion",
          "action": "Reveal animation is skipped"
        }
      ],
      "customization": [
        "columns number or per breakpoint",
        "gap: sm | md | lg",
        "reveal"
      ]
    },
    "examples": [
      "masonry-grid-demo",
      "masonry-grid-states"
    ],
    "url": "https://ui.ballmac.com/components/masonry-grid"
  },
  "categories": [
    "layout"
  ],
  "type": "registry:ui"
}