{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "product-grid-page",
  "title": "Product Grid Page",
  "type": "registry:page",
  "files": [
    {
      "path": "packages/registry-vue/blocks/product-grid-page/ProductGridPage.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed, ref } from 'vue'\nimport {\n  Check,\n  ChevronLeft,\n  ChevronRight,\n  Heart,\n  RotateCcw,\n  ShoppingBag,\n  SlidersHorizontal,\n  Star,\n  X,\n} from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { Badge } from '@/components/ui/badge'\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from '@/components/ui/breadcrumb'\nimport { Button } from '@/components/ui/button'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { Separator } from '@/components/ui/separator'\nimport { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'\nimport ProductFilterSidebar from './ProductFilterSidebar.vue'\nimport { CATEGORIES, COLORS } from './product-filter-data'\n\ninterface ProductColor {\n  id: string\n  name: string\n  class: string\n  colorCode: string\n}\n\ninterface Product {\n  id: string\n  name: string\n  brand: string\n  category: string\n  categoryId: string\n  price: number\n  originalPrice?: number\n  rating: number\n  reviewCount: number\n  badge?: {\n    label: string\n    variant: 'default' | 'secondary' | 'destructive' | 'outline'\n  }\n  image: string\n  colors: ProductColor[]\n  createdAt: number\n  featuredRank: number\n}\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n\nconst PRODUCTS: Product[] = [\n  {\n    id: 'prod-1',\n    name: 'XT-6 GORE-TEX Trail Running Shoe',\n    brand: 'Salomon',\n    category: 'Running',\n    categoryId: 'running',\n    price: 220,\n    originalPrice: 245,\n    rating: 4.9,\n    reviewCount: 128,\n    badge: { label: 'Best Seller', variant: 'default' },\n    image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=700&auto=format&fit=crop&q=80',\n    colors: [\n      { id: 'c1', name: 'Black', class: 'bg-zinc-950 dark:bg-zinc-900', colorCode: 'black' },\n      { id: 'c2', name: 'Sage', class: 'bg-chart-2', colorCode: 'sage' },\n      { id: 'c3', name: 'Navy', class: 'bg-slate-900', colorCode: 'navy' },\n    ],\n    createdAt: 1708000000000,\n    featuredRank: 1,\n  },\n  {\n    id: 'prod-2',\n    name: 'Air Max Pulse Premium',\n    brand: 'Nike',\n    category: 'Sneakers',\n    categoryId: 'sneakers',\n    price: 150,\n    originalPrice: 180,\n    rating: 4.8,\n    reviewCount: 94,\n    badge: { label: '-20% OFF', variant: 'destructive' },\n    image: 'https://images.unsplash.com/photo-1552346154-21d32810aba3?w=700&auto=format&fit=crop&q=80',\n    colors: [\n      { id: 'c4', name: 'White', class: 'bg-zinc-100 dark:bg-zinc-200 border border-zinc-300', colorCode: 'white' },\n      { id: 'c5', name: 'Black', class: 'bg-zinc-950 dark:bg-zinc-900', colorCode: 'black' },\n      { id: 'c6', name: 'Amber', class: 'bg-warning', colorCode: 'amber' },\n    ],\n    createdAt: 1708500000000,\n    featuredRank: 2,\n  },\n  {\n    id: 'prod-3',\n    name: '990v6 Made in USA Core Sneaker',\n    brand: 'New Balance',\n    category: 'Sneakers',\n    categoryId: 'sneakers',\n    price: 200,\n    rating: 4.9,\n    reviewCount: 215,\n    badge: { label: 'New', variant: 'secondary' },\n    image: 'https://images.unsplash.com/photo-1539185441755-769473a23570?w=700&auto=format&fit=crop&q=80',\n    colors: [\n      { id: 'c7', name: 'Sage', class: 'bg-chart-2', colorCode: 'sage' },\n      { id: 'c8', name: 'White', class: 'bg-zinc-100 dark:bg-zinc-200 border border-zinc-300', colorCode: 'white' },\n      { id: 'c9', name: 'Navy', class: 'bg-slate-900', colorCode: 'navy' },\n    ],\n    createdAt: 1709000000000,\n    featuredRank: 3,\n  },\n  {\n    id: 'prod-4',\n    name: 'Ultraboost Light Performance Runner',\n    brand: 'Adidas',\n    category: 'Running',\n    categoryId: 'running',\n    price: 190,\n    originalPrice: 210,\n    rating: 4.7,\n    reviewCount: 86,\n    image: 'https://images.unsplash.com/photo-1587563871167-1ee9c731aefb?w=700&auto=format&fit=crop&q=80',\n    colors: [\n      { id: 'c10', name: 'Black', class: 'bg-zinc-950 dark:bg-zinc-900', colorCode: 'black' },\n      { id: 'c11', name: 'White', class: 'bg-zinc-100 dark:bg-zinc-200 border border-zinc-300', colorCode: 'white' },\n    ],\n    createdAt: 1707000000000,\n    featuredRank: 4,\n  },\n  {\n    id: 'prod-5',\n    name: 'Tor Ultra Hi Waterproof Boot',\n    brand: 'Salomon',\n    category: 'Boots',\n    categoryId: 'boots',\n    price: 260,\n    rating: 4.8,\n    reviewCount: 62,\n    badge: { label: 'Trending', variant: 'outline' },\n    image: 'https://images.unsplash.com/photo-1608231387042-66d1773070a5?w=700&auto=format&fit=crop&q=80',\n    colors: [\n      { id: 'c12', name: 'Black', class: 'bg-zinc-950 dark:bg-zinc-900', colorCode: 'black' },\n      { id: 'c13', name: 'Amber', class: 'bg-warning', colorCode: 'amber' },\n    ],\n    createdAt: 1708800000000,\n    featuredRank: 5,\n  },\n  {\n    id: 'prod-6',\n    name: 'Penny Lug Leather Loafer',\n    brand: 'Adidas',\n    category: 'Loafers',\n    categoryId: 'loafers',\n    price: 160,\n    originalPrice: 190,\n    rating: 4.5,\n    reviewCount: 41,\n    badge: { label: 'Sale', variant: 'destructive' },\n    image: 'https://images.unsplash.com/photo-1533867617858-e7b97e060509?w=700&auto=format&fit=crop&q=80',\n    colors: [\n      { id: 'c14', name: 'Black', class: 'bg-zinc-950 dark:bg-zinc-900', colorCode: 'black' },\n      { id: 'c15', name: 'Navy', class: 'bg-slate-900', colorCode: 'navy' },\n    ],\n    createdAt: 1706000000000,\n    featuredRank: 6,\n  },\n  {\n    id: 'prod-7',\n    name: 'Fresh Foam X Trail 574 v2',\n    brand: 'New Balance',\n    category: 'Running',\n    categoryId: 'running',\n    price: 135,\n    originalPrice: 155,\n    rating: 4.6,\n    reviewCount: 78,\n    image: 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?w=700&auto=format&fit=crop&q=80',\n    colors: [\n      { id: 'c16', name: 'Sage', class: 'bg-chart-2', colorCode: 'sage' },\n      { id: 'c17', name: 'Amber', class: 'bg-warning', colorCode: 'amber' },\n      { id: 'c18', name: 'White', class: 'bg-zinc-100 dark:bg-zinc-200 border border-zinc-300', colorCode: 'white' },\n    ],\n    createdAt: 1707500000000,\n    featuredRank: 7,\n  },\n  {\n    id: 'prod-8',\n    name: 'Tech Fleece Runner Low Boot',\n    brand: 'Nike',\n    category: 'Boots',\n    categoryId: 'boots',\n    price: 185,\n    rating: 4.7,\n    reviewCount: 53,\n    badge: { label: 'New', variant: 'secondary' },\n    image: 'https://images.unsplash.com/photo-1514989940723-e8e51635b782?w=700&auto=format&fit=crop&q=80',\n    colors: [\n      { id: 'c19', name: 'Black', class: 'bg-zinc-950 dark:bg-zinc-900', colorCode: 'black' },\n      { id: 'c20', name: 'Navy', class: 'bg-slate-900', colorCode: 'navy' },\n      { id: 'c21', name: 'Sage', class: 'bg-chart-2', colorCode: 'sage' },\n    ],\n    createdAt: 1708900000000,\n    featuredRank: 8,\n  },\n]\n\nconst selectedCategories = ref<string[]>([])\nconst priceRange = ref<[number, number]>([50, 300])\nconst selectedColors = ref<string[]>([])\nconst selectedBrands = ref<string[]>([])\nconst minRating = ref<number>(0)\nconst sortBy = ref<string>('featured')\nconst wishlistIds = ref<Set<string>>(new Set(['prod-1', 'prod-5']))\nconst addedToCartIds = ref<Set<string>>(new Set())\nconst activeProductColor = ref<Record<string, string>>({})\nconst page = ref(1)\nconst totalPages = 3\nconst isMobileFilterOpen = ref(false)\n\nfunction toggleCategory(id: string, checked: boolean) {\n  if (checked) {\n    selectedCategories.value = [...selectedCategories.value, id]\n  } else {\n    selectedCategories.value = selectedCategories.value.filter((item) => item !== id)\n  }\n  page.value = 1\n}\n\nfunction toggleBrand(id: string, checked: boolean) {\n  if (checked) {\n    selectedBrands.value = [...selectedBrands.value, id]\n  } else {\n    selectedBrands.value = selectedBrands.value.filter((item) => item !== id)\n  }\n  page.value = 1\n}\n\nfunction toggleColor(id: string) {\n  if (selectedColors.value.includes(id)) {\n    selectedColors.value = selectedColors.value.filter((item) => item !== id)\n  } else {\n    selectedColors.value = [...selectedColors.value, id]\n  }\n  page.value = 1\n}\n\nfunction setRating(val: number) {\n  if (minRating.value === val) {\n    minRating.value = 0\n  } else {\n    minRating.value = val\n  }\n  page.value = 1\n}\n\nfunction toggleWishlist(id: string) {\n  const next = new Set(wishlistIds.value)\n  if (next.has(id)) {\n    next.delete(id)\n  } else {\n    next.add(id)\n  }\n  wishlistIds.value = next\n}\n\nfunction isWishlisted(id: string) {\n  return wishlistIds.value.has(id)\n}\n\nfunction addToCart(id: string) {\n  const next = new Set(addedToCartIds.value)\n  next.add(id)\n  addedToCartIds.value = next\n  setTimeout(() => {\n    const updated = new Set(addedToCartIds.value)\n    updated.delete(id)\n    addedToCartIds.value = updated\n  }, 1600)\n}\n\nfunction isAddedToCart(id: string) {\n  return addedToCartIds.value.has(id)\n}\n\nfunction selectProductColor(productId: string, colorCode: string) {\n  activeProductColor.value = {\n    ...activeProductColor.value,\n    [productId]: colorCode,\n  }\n}\n\nfunction resetFilters() {\n  selectedCategories.value = []\n  priceRange.value = [50, 300]\n  selectedColors.value = []\n  selectedBrands.value = []\n  minRating.value = 0\n  page.value = 1\n}\n\nconst hasActiveFilters = computed(() => {\n  return (\n    selectedCategories.value.length > 0 ||\n    selectedBrands.value.length > 0 ||\n    selectedColors.value.length > 0 ||\n    minRating.value > 0 ||\n    priceRange.value[0] > 50 ||\n    priceRange.value[1] < 300\n  )\n})\n\nconst activeFilterCount = computed(() => {\n  let count = 0\n  if (selectedCategories.value.length > 0) count += selectedCategories.value.length\n  if (selectedBrands.value.length > 0) count += selectedBrands.value.length\n  if (selectedColors.value.length > 0) count += selectedColors.value.length\n  if (minRating.value > 0) count += 1\n  if (priceRange.value[0] > 50 || priceRange.value[1] < 300) count += 1\n  return count\n})\n\nfunction getCategoryCount(catId: string) {\n  return PRODUCTS.filter((p) => p.categoryId === catId).length\n}\n\nfunction getBrandCount(brand: string) {\n  return PRODUCTS.filter((p) => p.brand === brand).length\n}\n\nconst filteredProducts = computed(() => {\n  return PRODUCTS.filter((product) => {\n    if (selectedCategories.value.length > 0 && !selectedCategories.value.includes(product.categoryId)) {\n      return false\n    }\n    if (product.price < priceRange.value[0] || product.price > priceRange.value[1]) {\n      return false\n    }\n    if (selectedColors.value.length > 0) {\n      const hasMatchingColor = product.colors.some((c) => selectedColors.value.includes(c.colorCode))\n      if (!hasMatchingColor) return false\n    }\n    if (selectedBrands.value.length > 0 && !selectedBrands.value.includes(product.brand)) {\n      return false\n    }\n    if (minRating.value > 0 && product.rating < minRating.value) {\n      return false\n    }\n    return true\n  }).sort((a, b) => {\n    if (sortBy.value === 'price-asc') return a.price - b.price\n    if (sortBy.value === 'price-desc') return b.price - a.price\n    if (sortBy.value === 'rating-desc') return b.rating - a.rating\n    if (sortBy.value === 'newest') return b.createdAt - a.createdAt\n    return a.featuredRank - b.featuredRank\n  })\n})\n</script>\n\n<template>\n  <div data-slot=\"product-grid-page\" :class=\"cn('w-full space-y-8', props.class)\">\n    <!-- Breadcrumb -->\n    <Breadcrumb>\n      <BreadcrumbList>\n        <BreadcrumbItem>\n          <BreadcrumbLink href=\"#\" class=\"inline-flex min-h-6 items-center\">Home</BreadcrumbLink>\n        </BreadcrumbItem>\n        <BreadcrumbSeparator />\n        <BreadcrumbItem>\n          <BreadcrumbLink href=\"#\" class=\"inline-flex min-h-6 items-center\">Shop</BreadcrumbLink>\n        </BreadcrumbItem>\n        <BreadcrumbSeparator />\n        <BreadcrumbItem>\n          <BreadcrumbPage>Footwear</BreadcrumbPage>\n        </BreadcrumbItem>\n      </BreadcrumbList>\n    </Breadcrumb>\n\n    <!-- Header & Toolbar -->\n    <div class=\"border-border flex flex-col gap-4 border-b pb-6 sm:flex-row sm:items-end sm:justify-between\">\n      <div class=\"space-y-1\">\n        <div class=\"flex items-center gap-3\">\n          <h1 class=\"text-foreground text-2xl font-bold tracking-tight sm:text-3xl\">All Footwear</h1>\n          <Badge variant=\"secondary\" class=\"font-normal tabular-nums\"> {{ filteredProducts.length }} items </Badge>\n        </div>\n        <p class=\"text-muted-foreground text-xs sm:text-sm\">\n          Engineered for performance, trail endurance, and everyday urban utility.\n        </p>\n      </div>\n\n      <div class=\"flex items-center gap-2\">\n        <!-- Mobile Filter Drawer Trigger -->\n        <Sheet v-model:open=\"isMobileFilterOpen\">\n          <SheetTrigger as-child>\n            <Button variant=\"outline\" size=\"sm\" class=\"gap-2 md:hidden\">\n              <SlidersHorizontal class=\"size-4\" />\n              <span>Filters</span>\n              <Badge v-if=\"activeFilterCount > 0\" variant=\"secondary\" class=\"ml-1 px-1.5 py-0 text-xs\">\n                {{ activeFilterCount }}\n              </Badge>\n            </Button>\n          </SheetTrigger>\n          <SheetContent side=\"left\" class=\"w-full max-w-xs overflow-y-auto p-6\">\n            <SheetHeader class=\"text-left\">\n              <div class=\"flex items-center justify-between\">\n                <SheetTitle class=\"text-base font-semibold\">Filters</SheetTitle>\n                <Button\n                  v-if=\"hasActiveFilters\"\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  class=\"text-muted-foreground hover:text-foreground h-auto p-0 text-xs font-normal\"\n                  @click=\"resetFilters\"\n                >\n                  Reset all\n                </Button>\n              </div>\n              <SheetDescription class=\"text-xs\"> Narrow down footwear by category, price, and specs. </SheetDescription>\n            </SheetHeader>\n\n            <div class=\"mt-6\">\n              <ProductFilterSidebar\n                is-mobile\n                :selected-categories=\"selectedCategories\"\n                :price-range=\"priceRange\"\n                :selected-colors=\"selectedColors\"\n                :selected-brands=\"selectedBrands\"\n                :min-rating=\"minRating\"\n                :get-category-count=\"getCategoryCount\"\n                :get-brand-count=\"getBrandCount\"\n                :on-toggle-category=\"toggleCategory\"\n                :on-price-range-change=\"(v) => (priceRange = v)\"\n                :on-toggle-color=\"toggleColor\"\n                :on-toggle-brand=\"toggleBrand\"\n                :on-select-rating=\"setRating\"\n              />\n            </div>\n          </SheetContent>\n        </Sheet>\n\n        <!-- Sort Select -->\n        <div class=\"flex items-center gap-2\">\n          <span class=\"text-muted-foreground hidden text-xs sm:inline-block\">Sort by:</span>\n          <Select v-model=\"sortBy\">\n            <SelectTrigger class=\"h-9 w-44 text-xs sm:text-sm\">\n              <SelectValue placeholder=\"Sort by\" />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectItem value=\"featured\">Featured</SelectItem>\n              <SelectItem value=\"price-asc\">Price: Low to High</SelectItem>\n              <SelectItem value=\"price-desc\">Price: High to Low</SelectItem>\n              <SelectItem value=\"rating-desc\">Highest Rated</SelectItem>\n              <SelectItem value=\"newest\">Newest</SelectItem>\n            </SelectContent>\n          </Select>\n        </div>\n      </div>\n    </div>\n\n    <!-- 2-Column Main Layout -->\n    <div class=\"grid grid-cols-1 gap-8 md:grid-cols-[240px_1fr] lg:grid-cols-[260px_1fr]\">\n      <!-- Left Desktop Sidebar -->\n      <aside class=\"hidden space-y-6 md:block\" aria-label=\"Product filters\">\n        <div class=\"flex items-center justify-between\">\n          <h2 class=\"text-foreground text-sm font-semibold tracking-tight\">Filters</h2>\n          <Button\n            v-if=\"hasActiveFilters\"\n            variant=\"ghost\"\n            size=\"sm\"\n            class=\"text-muted-foreground hover:text-foreground h-auto p-0 text-xs font-normal\"\n            @click=\"resetFilters\"\n          >\n            Reset all\n          </Button>\n        </div>\n\n        <Separator />\n\n        <ProductFilterSidebar\n          :selected-categories=\"selectedCategories\"\n          :price-range=\"priceRange\"\n          :selected-colors=\"selectedColors\"\n          :selected-brands=\"selectedBrands\"\n          :min-rating=\"minRating\"\n          :get-category-count=\"getCategoryCount\"\n          :get-brand-count=\"getBrandCount\"\n          :on-toggle-category=\"toggleCategory\"\n          :on-price-range-change=\"(v) => (priceRange = v)\"\n          :on-toggle-color=\"toggleColor\"\n          :on-toggle-brand=\"toggleBrand\"\n          :on-select-rating=\"setRating\"\n        />\n      </aside>\n\n      <!-- Right Product Grid Area -->\n      <section class=\"min-w-0 space-y-6\">\n        <!-- Active Filter Chips -->\n        <div v-if=\"hasActiveFilters\" class=\"flex flex-wrap items-center gap-2\">\n          <Badge v-for=\"catId in selectedCategories\" :key=\"catId\" variant=\"secondary\" class=\"gap-1.5 py-1 text-xs\">\n            <span>Category: {{ CATEGORIES.find((c) => c.id === catId)?.label }}</span>\n            <button\n              type=\"button\"\n              class=\"hover:text-foreground text-muted-foreground focus-visible:outline-none\"\n              aria-label=\"Remove category filter\"\n              @click=\"toggleCategory(catId, false)\"\n            >\n              <X class=\"size-3\" />\n            </button>\n          </Badge>\n\n          <Badge v-for=\"brand in selectedBrands\" :key=\"brand\" variant=\"secondary\" class=\"gap-1.5 py-1 text-xs\">\n            <span>Brand: {{ brand }}</span>\n            <button\n              type=\"button\"\n              class=\"hover:text-foreground text-muted-foreground focus-visible:outline-none\"\n              aria-label=\"Remove brand filter\"\n              @click=\"toggleBrand(brand, false)\"\n            >\n              <X class=\"size-3\" />\n            </button>\n          </Badge>\n\n          <Badge v-for=\"colorId in selectedColors\" :key=\"colorId\" variant=\"secondary\" class=\"gap-1.5 py-1 text-xs\">\n            <span>Color: {{ COLORS.find((c) => c.id === colorId)?.label }}</span>\n            <button\n              type=\"button\"\n              class=\"hover:text-foreground text-muted-foreground focus-visible:outline-none\"\n              aria-label=\"Remove color filter\"\n              @click=\"toggleColor(colorId)\"\n            >\n              <X class=\"size-3\" />\n            </button>\n          </Badge>\n\n          <Badge v-if=\"minRating > 0\" variant=\"secondary\" class=\"gap-1.5 py-1 text-xs\">\n            <span>Rating: {{ minRating }}+ Stars</span>\n            <button\n              type=\"button\"\n              class=\"hover:text-foreground text-muted-foreground focus-visible:outline-none\"\n              aria-label=\"Remove rating filter\"\n              @click=\"minRating = 0\"\n            >\n              <X class=\"size-3\" />\n            </button>\n          </Badge>\n\n          <Badge v-if=\"priceRange[0] > 50 || priceRange[1] < 300\" variant=\"secondary\" class=\"gap-1.5 py-1 text-xs\">\n            <span>Price: ${{ priceRange[0] }} - ${{ priceRange[1] }}</span>\n            <button\n              type=\"button\"\n              class=\"hover:text-foreground text-muted-foreground focus-visible:outline-none\"\n              aria-label=\"Reset price filter\"\n              @click=\"priceRange = [50, 300]\"\n            >\n              <X class=\"size-3\" />\n            </button>\n          </Badge>\n\n          <Button\n            variant=\"link\"\n            size=\"sm\"\n            class=\"text-muted-foreground hover:text-foreground h-auto px-1 text-xs\"\n            @click=\"resetFilters\"\n          >\n            Clear all\n          </Button>\n        </div>\n\n        <!-- Empty State -->\n        <div\n          v-if=\"filteredProducts.length === 0\"\n          class=\"border-border flex flex-col items-center justify-center rounded-xl border border-dashed py-16 text-center\"\n        >\n          <div class=\"bg-muted text-muted-foreground flex size-12 items-center justify-center rounded-full\">\n            <RotateCcw class=\"size-6\" />\n          </div>\n          <h3 class=\"text-foreground mt-4 text-base font-semibold\">No matching footwear found</h3>\n          <p class=\"text-muted-foreground mt-1 max-w-sm text-xs sm:text-sm\">\n            Try adjusting your price range, clearing selected colors, or resetting your filters to see more results.\n          </p>\n          <Button variant=\"outline\" size=\"sm\" class=\"mt-4 gap-2\" @click=\"resetFilters\">\n            <RotateCcw class=\"size-3.5\" />\n            <span>Reset filters</span>\n          </Button>\n        </div>\n\n        <!-- 3-Column Product Grid -->\n        <div v-else class=\"grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3\">\n          <div\n            v-for=\"product in filteredProducts\"\n            :key=\"product.id\"\n            class=\"group border-border bg-card hover:border-border/80 relative flex flex-col overflow-hidden rounded-xl border p-3 shadow-xs transition-colors duration-200 hover:shadow-md\"\n          >\n            <!-- Image Aspect Square Container -->\n            <div class=\"bg-muted/40 relative aspect-square w-full overflow-hidden rounded-lg\">\n              <img\n                :src=\"product.image\"\n                :alt=\"product.name\"\n                class=\"h-full w-full object-cover object-center transition-transform duration-500 ease-out group-hover:scale-105\"\n                loading=\"lazy\"\n              />\n\n              <!-- Top Left Badge -->\n              <div v-if=\"product.badge\" class=\"absolute top-2.5 left-2.5 z-10\">\n                <Badge :variant=\"product.badge.variant\" class=\"text-xs shadow-xs\">\n                  {{ product.badge.label }}\n                </Badge>\n              </div>\n\n              <!-- Top Right Wishlist Heart -->\n              <button\n                type=\"button\"\n                aria-label=\"Toggle wishlist\"\n                class=\"bg-background/80 hover:bg-background text-muted-foreground hover:text-foreground focus-visible:ring-ring absolute top-2.5 right-2.5 z-10 flex size-8 items-center justify-center rounded-full shadow-xs backdrop-blur-xs transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n                :class=\"{ 'text-destructive hover:text-destructive': isWishlisted(product.id) }\"\n                @click.stop=\"toggleWishlist(product.id)\"\n              >\n                <Heart\n                  class=\"size-4 transition-transform active:scale-90\"\n                  :class=\"{ 'fill-current': isWishlisted(product.id) }\"\n                />\n              </button>\n\n              <!-- Quick Add Button (Visible on hover & focus) -->\n              <div\n                class=\"absolute inset-x-2.5 bottom-2.5 z-10 opacity-100 sm:translate-y-2 sm:opacity-0 sm:transition-colors sm:duration-200 sm:group-hover:translate-y-0 sm:group-hover:opacity-100\"\n              >\n                <Button\n                  size=\"sm\"\n                  class=\"w-full gap-1.5 text-xs font-medium shadow-sm\"\n                  :variant=\"isAddedToCart(product.id) ? 'default' : 'default'\"\n                  @click.stop=\"addToCart(product.id)\"\n                >\n                  <Check v-if=\"isAddedToCart(product.id)\" class=\"size-3.5\" />\n                  <ShoppingBag v-else class=\"size-3.5\" />\n                  <span>{{ isAddedToCart(product.id) ? 'Added to Cart' : 'Quick Add' }}</span>\n                </Button>\n              </div>\n            </div>\n\n            <!-- Card Body -->\n            <div class=\"mt-3 flex flex-1 flex-col justify-between space-y-2\">\n              <div>\n                <!-- Color swatches preview -->\n                <div class=\"flex items-center gap-1.5\">\n                  <button\n                    v-for=\"color in product.colors\"\n                    :key=\"color.id\"\n                    type=\"button\"\n                    :title=\"color.name\"\n                    class=\"size-6 rounded-full transition-opacity hover:opacity-90 focus-visible:outline-none\"\n                    :class=\"[\n                      color.class,\n                      activeProductColor[product.id] === color.colorCode\n                        ? 'ring-1.5 ring-primary ring-offset-background ring-offset-1'\n                        : '',\n                    ]\"\n                    @click.stop=\"selectProductColor(product.id, color.colorCode)\"\n                  >\n                    <span class=\"sr-only\">{{ color.name }}</span>\n                  </button>\n                  <span class=\"text-muted-foreground ml-1 text-xs\">\n                    {{ product.colors.length }} {{ product.colors.length === 1 ? 'color' : 'colors' }}\n                  </span>\n                </div>\n\n                <!-- Brand & Category -->\n                <div class=\"text-muted-foreground mt-2 flex items-center justify-between text-xs\">\n                  <span>{{ product.brand }} · {{ product.category }}</span>\n                  <div class=\"flex items-center gap-1\">\n                    <Star class=\"fill-warning text-warning size-3\" />\n                    <span class=\"text-foreground font-medium\">{{ product.rating }}</span>\n                    <span class=\"text-muted-foreground\">({{ product.reviewCount }})</span>\n                  </div>\n                </div>\n\n                <!-- Title -->\n                <h3\n                  class=\"text-foreground group-hover:text-primary mt-1 line-clamp-1 cursor-pointer text-sm font-medium transition-colors\"\n                  :title=\"product.name\"\n                >\n                  {{ product.name }}\n                </h3>\n              </div>\n\n              <!-- Price Row -->\n              <div class=\"flex min-w-0 flex-wrap items-baseline gap-2 pt-1\">\n                <span class=\"text-foreground text-base font-semibold\"> ${{ product.price.toFixed(2) }} </span>\n                <span v-if=\"product.originalPrice\" class=\"text-muted-foreground text-xs line-through\">\n                  ${{ product.originalPrice.toFixed(2) }}\n                </span>\n                <span v-if=\"product.originalPrice\" class=\"text-success text-xs font-medium\">\n                  Save ${{ (product.originalPrice - product.price).toFixed(2) }}\n                </span>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- Pagination Footer -->\n        <nav\n          v-if=\"filteredProducts.length > 0\"\n          class=\"border-border flex items-center justify-between border-t pt-6\"\n          aria-label=\"Pagination\"\n        >\n          <Button variant=\"outline\" size=\"sm\" class=\"gap-1 text-xs\" :disabled=\"page === 1\" @click=\"page--\">\n            <ChevronLeft class=\"size-4\" />\n            <span>Previous</span>\n          </Button>\n\n          <div class=\"flex items-center gap-1\">\n            <Button\n              v-for=\"n in totalPages\"\n              :key=\"n\"\n              :variant=\"n === page ? 'outline' : 'ghost'\"\n              size=\"sm\"\n              class=\"size-8 p-0 text-xs tabular-nums\"\n              :aria-current=\"n === page ? 'page' : undefined\"\n              @click=\"page = n\"\n            >\n              {{ n }}\n            </Button>\n          </div>\n\n          <Button variant=\"outline\" size=\"sm\" class=\"gap-1 text-xs\" :disabled=\"page === totalPages\" @click=\"page++\">\n            <span>Next</span>\n            <ChevronRight class=\"size-4\" />\n          </Button>\n        </nav>\n      </section>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:page",
      "target": "~/app/components/blocks/ProductGridPage.vue"
    },
    {
      "path": "packages/registry-vue/blocks/product-grid-page/ProductFilterSidebar.vue",
      "content": "<script setup lang=\"ts\">\nimport { Check, Star } from 'lucide-vue-next'\nimport { Checkbox } from '@/components/ui/checkbox'\nimport { Separator } from '@/components/ui/separator'\nimport { Slider } from '@/components/ui/slider'\n\nimport { BRANDS, CATEGORIES, COLORS, RATING_OPTIONS } from './product-filter-data'\n\nconst props = withDefaults(\n  defineProps<{\n    isMobile?: boolean\n    selectedCategories: string[]\n    priceRange: [number, number]\n    selectedColors: string[]\n    selectedBrands: string[]\n    minRating: number\n    getCategoryCount: (id: string) => number\n    getBrandCount: (brand: string) => number\n    onToggleCategory: (id: string, checked: boolean) => void\n    onPriceRangeChange: (value: [number, number]) => void\n    onToggleColor: (id: string) => void\n    onToggleBrand: (id: string, checked: boolean) => void\n    onSelectRating: (value: number) => void\n  }>(),\n  { isMobile: false },\n)\n\nconst idPrefix = props.isMobile ? 'mobile-' : ''\n</script>\n\n<template>\n  <div class=\"space-y-6\">\n    <!-- Categories -->\n    <div class=\"space-y-3\">\n      <h3 class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">Category</h3>\n      <div class=\"space-y-2.5\">\n        <div v-for=\"cat in CATEGORIES\" :key=\"cat.id\" class=\"flex items-center justify-between\">\n          <label\n            :for=\"`${idPrefix}cat-${cat.id}`\"\n            class=\"text-foreground flex cursor-pointer items-center gap-2 text-sm\"\n          >\n            <Checkbox\n              :id=\"`${idPrefix}cat-${cat.id}`\"\n              :model-value=\"selectedCategories.includes(cat.id)\"\n              @update:model-value=\"onToggleCategory(cat.id, $event === true)\"\n            />\n            <span>{{ cat.label }}</span>\n          </label>\n          <span class=\"text-muted-foreground text-xs tabular-nums\">{{ getCategoryCount(cat.id) }}</span>\n        </div>\n      </div>\n    </div>\n\n    <Separator />\n\n    <!-- Price Range -->\n    <div class=\"space-y-3\">\n      <div class=\"flex items-center justify-between\">\n        <h3 class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">Price Range</h3>\n        <span class=\"text-foreground text-xs font-medium tabular-nums\">\n          ${{ priceRange[0] }} – ${{ priceRange[1] }}\n        </span>\n      </div>\n      <Slider\n        :model-value=\"priceRange\"\n        :min=\"50\"\n        :max=\"300\"\n        :step=\"5\"\n        :range=\"true\"\n        class=\"py-2\"\n        @update:model-value=\"onPriceRangeChange($event as [number, number])\"\n      />\n      <div class=\"text-muted-foreground flex items-center justify-between text-xs\">\n        <span>$50</span>\n        <span>$300</span>\n      </div>\n    </div>\n\n    <Separator />\n\n    <!-- Color Swatches -->\n    <div class=\"space-y-3\">\n      <h3 class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">Color</h3>\n      <div class=\"flex flex-wrap gap-2\">\n        <button\n          v-for=\"color in COLORS\"\n          :key=\"color.id\"\n          type=\"button\"\n          :title=\"color.label\"\n          class=\"focus-visible:ring-ring relative flex items-center justify-center rounded-full border transition-transform hover:scale-110 focus-visible:ring-2 focus-visible:outline-none\"\n          :class=\"[\n            isMobile ? 'size-8' : 'size-7',\n            color.class,\n            selectedColors.includes(color.id)\n              ? 'ring-primary ring-offset-background ring-2 ring-offset-2'\n              : 'border-border',\n          ]\"\n          @click=\"onToggleColor(color.id)\"\n        >\n          <Check\n            v-if=\"selectedColors.includes(color.id)\"\n            :class=\"[isMobile ? 'size-4' : 'size-3.5', color.id === 'white' ? 'text-zinc-950' : 'text-white']\"\n          />\n          <span class=\"sr-only\">{{ color.label }}</span>\n        </button>\n      </div>\n    </div>\n\n    <Separator />\n\n    <!-- Brand -->\n    <div class=\"space-y-3\">\n      <h3 class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">Brand</h3>\n      <div class=\"space-y-2.5\">\n        <div v-for=\"b in BRANDS\" :key=\"b.id\" class=\"flex items-center justify-between\">\n          <label\n            :for=\"`${idPrefix}brand-${b.id}`\"\n            class=\"text-foreground flex cursor-pointer items-center gap-2 text-sm\"\n          >\n            <Checkbox\n              :id=\"`${idPrefix}brand-${b.id}`\"\n              :model-value=\"selectedBrands.includes(b.id)\"\n              @update:model-value=\"onToggleBrand(b.id, $event === true)\"\n            />\n            <span>{{ b.label }}</span>\n          </label>\n          <span class=\"text-muted-foreground text-xs tabular-nums\">{{ getBrandCount(b.id) }}</span>\n        </div>\n      </div>\n    </div>\n\n    <Separator />\n\n    <!-- Rating -->\n    <div class=\"space-y-3\">\n      <h3 class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">\n        {{ isMobile ? 'Rating' : 'Customer Rating' }}\n      </h3>\n      <div :class=\"isMobile ? 'space-y-2' : 'space-y-1.5'\">\n        <button\n          v-for=\"opt in RATING_OPTIONS\"\n          :key=\"opt.value\"\n          type=\"button\"\n          class=\"hover:bg-muted/60 flex w-full items-center justify-between rounded-md px-2 py-1.5 text-left text-xs transition-colors\"\n          :class=\"minRating === opt.value ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground'\"\n          @click=\"onSelectRating(opt.value)\"\n        >\n          <div class=\"flex items-center gap-1.5\">\n            <Star class=\"fill-warning text-warning size-3.5\" />\n            <span>{{ opt.label }}</span>\n          </div>\n          <Check v-if=\"minRating === opt.value\" class=\"text-primary size-3.5\" />\n        </button>\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:page",
      "target": "~/app/components/blocks/ProductFilterSidebar.vue"
    },
    {
      "path": "packages/registry-vue/blocks/product-grid-page/product-filter-data.ts",
      "content": "export interface CategoryOption {\n  id: string\n  label: string\n}\n\nexport interface BrandOption {\n  id: string\n  label: string\n}\n\nexport interface ColorOption {\n  id: string\n  label: string\n  class: string\n}\n\nexport const CATEGORIES: CategoryOption[] = [\n  { id: 'sneakers', label: 'Sneakers' },\n  { id: 'running', label: 'Running' },\n  { id: 'boots', label: 'Boots' },\n  { id: 'loafers', label: 'Loafers' },\n]\n\nexport const BRANDS: BrandOption[] = [\n  { id: 'Nike', label: 'Nike' },\n  { id: 'Adidas', label: 'Adidas' },\n  { id: 'New Balance', label: 'New Balance' },\n  { id: 'Salomon', label: 'Salomon' },\n]\n\nexport const COLORS: ColorOption[] = [\n  { id: 'black', label: 'Black', class: 'bg-zinc-950 dark:bg-zinc-900 border-zinc-800' },\n  { id: 'white', label: 'White', class: 'bg-zinc-100 dark:bg-zinc-200 border-zinc-300' },\n  { id: 'sage', label: 'Sage', class: 'bg-chart-2 border-chart-2' },\n  { id: 'navy', label: 'Navy', class: 'bg-slate-900 border-slate-800' },\n  { id: 'amber', label: 'Amber', class: 'bg-warning border-warning' },\n]\n\nexport const RATING_OPTIONS = [\n  { value: 4.8, label: '4.8 stars & above' },\n  { value: 4.5, label: '4.5 stars & above' },\n  { value: 4.0, label: '4.0 stars & above' },\n]\n",
      "type": "registry:page",
      "target": "~/app/components/blocks/product-filter-data.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/breadcrumb.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/checkbox.json",
    "https://uipkge.dev/r/vue/select.json",
    "https://uipkge.dev/r/vue/separator.json",
    "https://uipkge.dev/r/vue/sheet.json",
    "https://uipkge.dev/r/vue/slider.json"
  ],
  "description": "E-commerce product listing page (PLP) featuring breadcrumb navigation, category header with sorting and mobile filter drawer, a 2-column layout with a multi-facet sidebar (categories, price range slider, color swatches, brands, ratings), and a responsive 3-column product grid with image hover zoom, badges, wishlist toggle, color swatches, quick add to cart, and pagination.",
  "categories": [
    "commerce",
    "ecommerce"
  ]
}