{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "collection-grid",
  "type": "registry:block",
  "title": "Filterable collection grid",
  "description": "Responsive product grid with result count, sort select and form filter chips; tiles reflow with a layout animation.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "collection",
    "any"
  ],
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://storekit-eight.vercel.app/r/cn.json",
    "https://storekit-eight.vercel.app/r/product-card-minimal.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "collection/collection-grid.tsx",
      "content": "\"use client\"\n\nimport { useId, useMemo, useState, type RefObject } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { ChevronDown } from \"lucide-react\"\nimport { cn } from \"@/components/storekit/lib/cn\"\nimport { ProductCardMinimal, type MinimalProduct } from \"@/components/storekit/product-card/product-card-minimal\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Collection grid with a toolbar: live result count, sort select and filter\n   chips that filter by category (or by form, if a filter sets one). Tiles reflow with a layout animation\n   when the filter or sort changes. 2, 3 then 4 columns. */\n\nconst EASE = [0.23, 1, 0.32, 1] as const\n\nexport type GridProduct = MinimalProduct & { id: string; form?: string }\ntype Filter = { label: string; category?: string; form?: string }\ntype Sort = \"featured\" | \"price-asc\" | \"price-desc\"\n\nconst SORTS: { value: Sort; label: string }[] = [\n  { value: \"featured\", label: \"Featured\" },\n  { value: \"price-asc\", label: \"Price, low to high\" },\n  { value: \"price-desc\", label: \"Price, high to low\" },\n]\n\nexport function CollectionGrid({\n  products = [],\n  filters = [\n    { label: \"All\" },\n    { label: \"Powders\", category: \"Powders\" },\n    { label: \"Capsules & gummies\", category: \"Capsules & gummies\" },\n    { label: \"Drinks\", category: \"Drinks\" },\n  ],\n  onAdd,\n  className,\n}: {\n  products?: GridProduct[]\n  filters?: Filter[]\n  onAdd?: (product: MinimalProduct) => void | Promise<void>\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sortId = useId()\n  const sk = useSkBlock(\"collection\", \"collection-grid\")\n  const [active, setActive] = useState(0)\n  const [sort, setSort] = useState<Sort>(\"featured\")\n\n  const visible = useMemo(() => {\n    const f = filters[active]\n    const list = products.filter((p) => (!f?.category || p.category === f.category) && (!f?.form || p.form === f.form))\n    if (sort === \"price-asc\") list.sort((a, b) => a.price - b.price)\n    if (sort === \"price-desc\") list.sort((a, b) => b.price - a.price)\n    return list\n  }, [products, filters, active, sort])\n\n  return (\n    <div ref={sk.ref as RefObject<HTMLDivElement>} {...sk.attrs} className={cn(\"flex flex-col gap-6\", className)}>\n      <div className=\"flex flex-col gap-4 border-b border-sk-line pb-4 sm:flex-row sm:items-center sm:justify-between\">\n        <div className=\"-mx-1 flex gap-2 overflow-x-auto px-1 [scrollbar-width:none]\" role=\"group\" aria-label=\"Filter by type\">\n          {filters.map((f, i) => (\n            <button\n              key={f.label}\n              type=\"button\"\n              aria-pressed={i === active}\n              onClick={() => {\n                if (i !== active) sk.emit(\"interact\", `filter:${f.label}`)\n                setActive(i)\n              }}\n              className={cn(\n                \"h-11 shrink-0 rounded-sk-chip border px-4 text-[13px] font-medium outline-none transition-[transform,background-color,color,border-color] duration-200 ease-sk-out active:scale-[0.97] sm:h-9\",\n                \"focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper\",\n                i === active ? \"border-sk-ink bg-sk-ink text-sk-paper\" : \"border-sk-line text-sk-ink hover:border-sk-ink/40\"\n              )}\n            >\n              {f.label}\n            </button>\n          ))}\n        </div>\n        <div className=\"flex items-center justify-between gap-4 sm:justify-end\">\n          <p className=\"shrink-0 whitespace-nowrap font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted tabular-nums\" aria-live=\"polite\">\n            {visible.length} {visible.length === 1 ? \"product\" : \"products\"}\n          </p>\n          <label htmlFor={sortId} className=\"sr-only\">\n            Sort by\n          </label>\n          <div className=\"relative min-w-0\">\n            <select\n              id={sortId}\n              value={sort}\n              onChange={(e) => {\n                sk.emit(\"interact\", `sort:${e.target.value}`)\n                setSort(e.target.value as Sort)\n              }}\n              className=\"h-11 w-full max-w-full truncate appearance-none rounded-sk-btn border border-sk-line bg-sk-card pl-3.5 pr-10 text-base text-sk-ink outline-none sm:text-[13px] transition-[border-color] duration-200 hover:border-sk-ink/30 focus-visible:border-sk-accent focus-visible:ring-[3px] focus-visible:ring-sk-accent/15 sm:h-9\"\n            >\n              {SORTS.map((s) => (\n                <option key={s.value} value={s.value}>\n                  {s.label}\n                </option>\n              ))}\n            </select>\n            <ChevronDown size={14} aria-hidden className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-sk-muted\" />\n          </div>\n        </div>\n      </div>\n\n      <motion.ul layout={!reduce} className=\"grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 lg:grid-cols-4\">\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {visible.map((p) => (\n            <motion.li\n              key={p.id}\n              layout={!reduce}\n              initial={{ opacity: 0, scale: reduce ? 1 : 0.95 }}\n              animate={{ opacity: 1, scale: 1 }}\n              exit={{ opacity: 0, scale: reduce ? 1 : 0.95 }}\n              transition={{ duration: 0.35, ease: EASE }}\n            >\n              <ProductCardMinimal product={p} onAdd={onAdd} />\n            </motion.li>\n          ))}\n        </AnimatePresence>\n      </motion.ul>\n      {visible.length === 0 ? <p className=\"py-12 text-center text-sm text-sk-muted\">Nothing in this filter yet.</p> : null}\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/collection/collection-grid.tsx"
    }
  ],
  "meta": {
    "section": "collection",
    "variant": "grid",
    "niche": [
      "any"
    ],
    "commerce": [
      "atc",
      "filters",
      "sort"
    ],
    "previewHeight": 760
  }
}