{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "product-grid",
  "title": "Product Grid",
  "author": "ecomcn <hello@ecomcn.dev>",
  "description": "Responsive catalogue grid with box-matched loading skeletons, appended skeletons while more results load, a density switch, a renderCard slot for composed cards, and a left-aligned empty state.",
  "registryDependencies": [
    "skeleton",
    "button",
    "@ecomcn/product-card"
  ],
  "files": [
    {
      "path": "src/registry/ecomcn/product-grid/product-grid.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n  ProductCard,\n  type ProductCardProduct,\n  type ProductCardProps,\n} from \"@/components/ecomcn/product-card\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface ProductGridProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n  products: ProductCardProduct[]\n  loading?: boolean\n  /** How many skeletons to render while `loading`. Match your page size. */\n  skeletonCount?: number\n  density?: \"comfortable\" | \"compact\"\n  currency?: string\n  locale?: string\n  onQuickAdd?: ProductCardProps[\"onQuickAdd\"]\n  /** Rendered instead of the grid when `products` is empty and not loading. */\n  empty?: React.ReactNode\n  onResetFilters?: () => void\n  /**\n   * Render each product yourself — typically a ProductCard composed from its\n   * parts. Return one element per product: it becomes a direct grid child.\n   */\n  renderCard?: (product: ProductCardProduct, index: number) => React.ReactNode\n  /** More results are on their way: append skeletons after the current products. */\n  loadingMore?: boolean\n  /** How many skeletons to append while `loadingMore`. Match your page size. */\n  loadingMoreCount?: number\n}\n\n// Skeleton boxes must match the real card exactly or the grid jumps: same\n// aspect ratio, same four text bars.\nfunction CardSkeleton() {\n  return (\n    <div aria-hidden data-slot=\"product-grid-skeleton\">\n      <Skeleton className=\"aspect-[4/5] w-full rounded-none\" />\n      <Skeleton className=\"mt-3.5 h-3 w-16 rounded-none\" />\n      <Skeleton className=\"mt-2 h-5 w-3/4 rounded-none\" />\n      <Skeleton className=\"mt-2 h-3 w-14 rounded-none\" />\n    </div>\n  )\n}\n\nexport function ProductGrid({\n  products,\n  loading,\n  skeletonCount = 6,\n  density = \"comfortable\",\n  currency,\n  locale,\n  onQuickAdd,\n  empty,\n  onResetFilters,\n  renderCard,\n  loadingMore,\n  loadingMoreCount = 3,\n  className,\n  ...props\n}: ProductGridProps) {\n  // Column gap tighter than row gap: products group by row, like a printed page.\n  // Compact adds a column from `sm` up, so a density switch always does something.\n  const grid = cn(\n    \"grid grid-cols-2 gap-x-5 gap-y-9 lg:grid-cols-3\",\n    density === \"compact\" && \"gap-y-7 sm:grid-cols-3 lg:grid-cols-4\",\n    className\n  )\n\n  if (loading) {\n    return (\n      <div className={grid} aria-busy=\"true\" data-slot=\"product-grid\" {...props}>\n        {Array.from({ length: skeletonCount }).map((_, i) => (\n          <CardSkeleton key={i} />\n        ))}\n        <span className=\"sr-only\">Loading products</span>\n      </div>\n    )\n  }\n\n  if (products.length === 0) {\n    return (\n      empty ?? (\n        <div className=\"flex flex-col items-start gap-5 border py-14 pl-8 pr-6 sm:pl-14\">\n          <h3 className=\"text-3xl\">Nothing under these filters.</h3>\n          <p className=\"max-w-sm text-sm leading-relaxed text-muted-foreground\">\n            Be specific here in your own copy — naming the filter that emptied the\n            grid converts far better than the words &ldquo;no results&rdquo;.\n          </p>\n          {onResetFilters ? (\n            <Button onClick={onResetFilters} className=\"h-10 text-[13px]\">\n              Reset filters\n            </Button>\n          ) : null}\n        </div>\n      )\n    )\n  }\n\n  return (\n    <div className={grid} aria-busy={loadingMore || undefined} data-slot=\"product-grid\" {...props}>\n      {products.map((product, index) =>\n        renderCard ? (\n          <React.Fragment key={product.id}>{renderCard(product, index)}</React.Fragment>\n        ) : (\n          <ProductCard\n            key={product.id}\n            product={product}\n            density={density}\n            currency={currency}\n            locale={locale}\n            onQuickAdd={onQuickAdd}\n          />\n        )\n      )}\n      {loadingMore\n        ? Array.from({ length: loadingMoreCount }).map((_, i) => <CardSkeleton key={`more-${i}`} />)\n        : null}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ecomcn/product-grid.tsx"
    }
  ],
  "categories": [
    "ecommerce",
    "catalog",
    "listing"
  ],
  "type": "registry:block"
}