{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "product-grid-page",
  "title": "Product Grid Page",
  "type": "registry:page",
  "files": [
    {
      "path": "packages/registry-angular/blocks/product-grid-page/product-grid-page.component.ts",
      "content": "// E-commerce product listing page: breadcrumb, category header with sort + mobile filter\n// drawer, multi-facet sidebar (categories, price range, colors, brands, ratings), product\n// grid with badges / wishlist / quick add, and pagination. Self-contained single-file port\n// of the Vue ProductGridPage + ProductFilterSidebar + product-filter-data 1:1.\nimport { NgTemplateOutlet } from '@angular/common'\nimport { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'\nimport {\n  Check,\n  ChevronLeft,\n  ChevronRight,\n  Heart,\n  LucideAngularModule,\n  RotateCcw,\n  ShoppingBag,\n  SlidersHorizontal,\n  Star,\n  X,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport {\n  UiBreadcrumbComponent,\n  UiBreadcrumbItemComponent,\n  UiBreadcrumbLinkComponent,\n  UiBreadcrumbListComponent,\n  UiBreadcrumbPageComponent,\n  UiBreadcrumbSeparatorComponent,\n} from '@/ui/breadcrumb/breadcrumb.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component'\nimport {\n  UiSelectComponent,\n  UiSelectContentComponent,\n  UiSelectItemComponent,\n  UiSelectTriggerComponent,\n  UiSelectValueComponent,\n} from '@/ui/select/select.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport {\n  UiSheetComponent,\n  UiSheetContentComponent,\n  UiSheetDescriptionComponent,\n  UiSheetHeaderComponent,\n  UiSheetTitleComponent,\n} from '@/ui/sheet/sheet.component'\nimport { UiSliderComponent } from '@/ui/slider/slider.component'\n\nexport 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\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 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\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-product-grid-page, [ui-product-grid-page]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    NgTemplateOutlet,\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiBreadcrumbComponent,\n    UiBreadcrumbItemComponent,\n    UiBreadcrumbLinkComponent,\n    UiBreadcrumbListComponent,\n    UiBreadcrumbPageComponent,\n    UiBreadcrumbSeparatorComponent,\n    UiButtonComponent,\n    UiCheckboxComponent,\n    UiSelectComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n    UiSeparatorComponent,\n    UiSheetComponent,\n    UiSheetContentComponent,\n    UiSheetDescriptionComponent,\n    UiSheetHeaderComponent,\n    UiSheetTitleComponent,\n    UiSliderComponent,\n  ],\n  template: `\n    <ng-template #filters let-prefix=\"prefix\">\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            @for (cat of filterCategories; track cat.id) {\n              <div class=\"flex items-center justify-between\">\n                <label\n                  [for]=\"prefix + 'cat-' + cat.id\"\n                  class=\"text-foreground flex cursor-pointer items-center gap-2 text-sm\"\n                >\n                  <ui-checkbox\n                    [id]=\"prefix + 'cat-' + cat.id\"\n                    [checked]=\"selectedCategories().includes(cat.id)\"\n                    (checkedChange)=\"toggleCategory(cat.id, !!$event)\"\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            }\n          </div>\n        </div>\n\n        <ui-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          <ui-slider\n            [value]=\"priceRange()\"\n            [min]=\"50\"\n            [max]=\"300\"\n            [step]=\"5\"\n            [range]=\"true\"\n            class=\"py-2\"\n            (valueChange)=\"setPriceRange($event)\"\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        <ui-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            @for (color of filterColors; track color.id) {\n              <button\n                type=\"button\"\n                [title]=\"color.label\"\n                [class]=\"filterColorClass(color)\"\n                (click)=\"toggleColor(color.id)\"\n              >\n                @if (selectedColors().includes(color.id)) {\n                  <lucide-icon\n                    [img]=\"Check\"\n                    [class]=\"color.id === 'white' ? 'size-3.5 text-zinc-950' : 'size-3.5 text-white'\"\n                  />\n                }\n                <span class=\"sr-only\">{{ color.label }}</span>\n              </button>\n            }\n          </div>\n        </div>\n\n        <ui-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            @for (b of filterBrands; track b.id) {\n              <div class=\"flex items-center justify-between\">\n                <label\n                  [for]=\"prefix + 'brand-' + b.id\"\n                  class=\"text-foreground flex cursor-pointer items-center gap-2 text-sm\"\n                >\n                  <ui-checkbox\n                    [id]=\"prefix + 'brand-' + b.id\"\n                    [checked]=\"selectedBrands().includes(b.id)\"\n                    (checkedChange)=\"toggleBrand(b.id, !!$event)\"\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            }\n          </div>\n        </div>\n\n        <ui-separator />\n\n        <!-- Rating -->\n        <div class=\"space-y-3\">\n          <h3 class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">\n            {{ prefix ? 'Rating' : 'Customer Rating' }}\n          </h3>\n          <div [class]=\"prefix ? 'space-y-2' : 'space-y-1.5'\">\n            @for (opt of ratingOptions; track opt.value) {\n              <button type=\"button\" [class]=\"ratingOptionClass(opt.value)\" (click)=\"setRating(opt.value)\">\n                <span class=\"flex items-center gap-1.5\">\n                  <lucide-icon [img]=\"Star\" class=\"fill-warning text-warning size-3.5\" />\n                  <span>{{ opt.label }}</span>\n                </span>\n                @if (minRating() === opt.value) {\n                  <lucide-icon [img]=\"Check\" class=\"text-primary size-3.5\" />\n                }\n              </button>\n            }\n          </div>\n        </div>\n      </div>\n    </ng-template>\n\n    <div data-slot=\"product-grid-page\" [class]=\"rootClass\">\n      <!-- Breadcrumb -->\n      <ui-breadcrumb>\n        <ui-breadcrumb-list>\n          <ui-breadcrumb-item>\n            <ui-breadcrumb-link href=\"#\" class=\"inline-flex min-h-6 items-center\">Home</ui-breadcrumb-link>\n          </ui-breadcrumb-item>\n          <ui-breadcrumb-separator />\n          <ui-breadcrumb-item>\n            <ui-breadcrumb-link href=\"#\" class=\"inline-flex min-h-6 items-center\">Shop</ui-breadcrumb-link>\n          </ui-breadcrumb-item>\n          <ui-breadcrumb-separator />\n          <ui-breadcrumb-item>\n            <ui-breadcrumb-page>Footwear</ui-breadcrumb-page>\n          </ui-breadcrumb-item>\n        </ui-breadcrumb-list>\n      </ui-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            <span ui-badge variant=\"secondary\" class=\"font-normal tabular-nums\">\n              {{ filteredProducts().length }} items\n            </span>\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          <div ui-sheet [open]=\"isMobileFilterOpen()\" (openChange)=\"isMobileFilterOpen.set($event)\">\n            <button\n              ui-button\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"gap-2 md:hidden\"\n              (click)=\"isMobileFilterOpen.set(true)\"\n            >\n              <lucide-icon [img]=\"SlidersHorizontal\" class=\"size-4\" />\n              <span>Filters</span>\n              @if (activeFilterCount() > 0) {\n                <span ui-badge variant=\"secondary\" class=\"ml-1 px-1.5 py-0 text-xs\">\n                  {{ activeFilterCount() }}\n                </span>\n              }\n            </button>\n            <div ui-sheet-content side=\"left\" class=\"w-full max-w-xs overflow-y-auto p-6\">\n              <div ui-sheet-header class=\"text-left\">\n                <div class=\"flex items-center justify-between\">\n                  <h2 ui-sheet-title class=\"text-base font-semibold\">Filters</h2>\n                  @if (hasActiveFilters()) {\n                    <button\n                      ui-button\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                  }\n                </div>\n                <p ui-sheet-description class=\"text-xs\">Narrow down footwear by category, price, and specs.</p>\n              </div>\n\n              <div class=\"mt-6\">\n                <ng-container *ngTemplateOutlet=\"filters; context: { prefix: 'mobile-' }\"></ng-container>\n              </div>\n            </div>\n          </div>\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            <ui-select [value]=\"sortBy()\" (valueChange)=\"setSortBy($event)\">\n              <button ui-select-trigger class=\"h-9 w-44 text-xs sm:text-sm\">\n                <ui-select-value placeholder=\"Sort by\" />\n              </button>\n              <ui-select-content>\n                <ui-select-item value=\"featured\">Featured</ui-select-item>\n                <ui-select-item value=\"price-asc\">Price: Low to High</ui-select-item>\n                <ui-select-item value=\"price-desc\">Price: High to Low</ui-select-item>\n                <ui-select-item value=\"rating-desc\">Highest Rated</ui-select-item>\n                <ui-select-item value=\"newest\">Newest</ui-select-item>\n              </ui-select-content>\n            </ui-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            @if (hasActiveFilters()) {\n              <button\n                ui-button\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            }\n          </div>\n\n          <ui-separator />\n\n          <ng-container *ngTemplateOutlet=\"filters; context: { prefix: '' }\"></ng-container>\n        </aside>\n\n        <!-- Right Product Grid Area -->\n        <section class=\"min-w-0 space-y-6\">\n          <!-- Active Filter Chips -->\n          @if (hasActiveFilters()) {\n            <div class=\"flex flex-wrap items-center gap-2\">\n              @for (catId of selectedCategories(); track catId) {\n                <span ui-badge variant=\"secondary\" class=\"gap-1.5 py-1 text-xs\">\n                  <span>Category: {{ categoryLabel(catId) }}</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                    <lucide-icon [img]=\"X\" class=\"size-3\" />\n                  </button>\n                </span>\n              }\n\n              @for (brand of selectedBrands(); track brand) {\n                <span ui-badge 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                    <lucide-icon [img]=\"X\" class=\"size-3\" />\n                  </button>\n                </span>\n              }\n\n              @for (colorId of selectedColors(); track colorId) {\n                <span ui-badge variant=\"secondary\" class=\"gap-1.5 py-1 text-xs\">\n                  <span>Color: {{ colorLabel(colorId) }}</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                    <lucide-icon [img]=\"X\" class=\"size-3\" />\n                  </button>\n                </span>\n              }\n\n              @if (minRating() > 0) {\n                <span ui-badge 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.set(0)\"\n                  >\n                    <lucide-icon [img]=\"X\" class=\"size-3\" />\n                  </button>\n                </span>\n              }\n\n              @if (priceRange()[0] > 50 || priceRange()[1] < 300) {\n                <span ui-badge 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.set([50, 300])\"\n                  >\n                    <lucide-icon [img]=\"X\" class=\"size-3\" />\n                  </button>\n                </span>\n              }\n\n              <button\n                ui-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\n          <!-- Empty State -->\n          @if (filteredProducts().length === 0) {\n            <div\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                <lucide-icon [img]=\"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 ui-button variant=\"outline\" size=\"sm\" class=\"mt-4 gap-2\" (click)=\"resetFilters()\">\n                <lucide-icon [img]=\"RotateCcw\" class=\"size-3.5\" />\n                <span>Reset filters</span>\n              </button>\n            </div>\n          } @else {\n            <!-- 3-Column Product Grid -->\n            <div class=\"grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3\">\n              @for (product of filteredProducts(); track product.id) {\n                <div\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                  <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                    @if (product.badge) {\n                      <div class=\"absolute top-2.5 left-2.5 z-10\">\n                        <span ui-badge [variant]=\"product.badge.variant\" class=\"text-xs shadow-xs\">\n                          {{ product.badge.label }}\n                        </span>\n                      </div>\n                    }\n\n                    <button\n                      type=\"button\"\n                      aria-label=\"Toggle wishlist\"\n                      [class]=\"wishlistClass(product.id)\"\n                      (click)=\"toggleWishlist(product.id)\"\n                    >\n                      <lucide-icon\n                        [img]=\"Heart\"\n                        class=\"size-4 transition-transform active:scale-90\"\n                        [class.fill-current]=\"isWishlisted(product.id)\"\n                      />\n                    </button>\n\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                        ui-button\n                        size=\"sm\"\n                        class=\"w-full gap-1.5 text-xs font-medium shadow-sm\"\n                        (click)=\"addToCart(product.id)\"\n                      >\n                        @if (isAddedToCart(product.id)) {\n                          <lucide-icon [img]=\"Check\" class=\"size-3.5\" />\n                        } @else {\n                          <lucide-icon [img]=\"ShoppingBag\" class=\"size-3.5\" />\n                        }\n                        <span>{{ isAddedToCart(product.id) ? 'Added to Cart' : 'Quick Add' }}</span>\n                      </button>\n                    </div>\n                  </div>\n\n                  <div class=\"mt-3 flex flex-1 flex-col justify-between space-y-2\">\n                    <div>\n                      <div class=\"flex items-center gap-1.5\">\n                        @for (color of product.colors; track color.id) {\n                          <button\n                            type=\"button\"\n                            [title]=\"color.name\"\n                            [class]=\"cardColorClass(product.id, color)\"\n                            (click)=\"selectProductColor(product.id, color.colorCode)\"\n                          >\n                            <span class=\"sr-only\">{{ color.name }}</span>\n                          </button>\n                        }\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                      <div class=\"text-muted-foreground mt-2 flex items-center justify-between text-xs\">\n                        <span>{{ product.brand }} · {{ product.category }}</span>\n                        <span class=\"flex items-center gap-1\">\n                          <lucide-icon [img]=\"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                        </span>\n                      </div>\n\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                    <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                      @if (product.originalPrice) {\n                        <span class=\"text-muted-foreground text-xs line-through\">\n                          \\${{ product.originalPrice.toFixed(2) }}\n                        </span>\n                        <span class=\"text-success text-xs font-medium\">\n                          Save \\${{ (product.originalPrice - product.price).toFixed(2) }}\n                        </span>\n                      }\n                    </div>\n                  </div>\n                </div>\n              }\n            </div>\n          }\n\n          <!-- Pagination Footer -->\n          @if (filteredProducts().length > 0) {\n            <nav class=\"border-border flex items-center justify-between border-t pt-6\" aria-label=\"Pagination\">\n              <button\n                ui-button\n                variant=\"outline\"\n                size=\"sm\"\n                class=\"gap-1 text-xs\"\n                [disabled]=\"page() === 1\"\n                (click)=\"page.update((p) => Math.max(1, p - 1))\"\n              >\n                <lucide-icon [img]=\"ChevronLeft\" class=\"size-4\" />\n                <span>Previous</span>\n              </button>\n\n              <div class=\"flex items-center gap-1\">\n                @for (n of totalPagesArray; track n) {\n                  <button\n                    ui-button\n                    [variant]=\"n === page() ? 'outline' : 'ghost'\"\n                    size=\"sm\"\n                    class=\"size-8 p-0 text-xs tabular-nums\"\n                    [attr.aria-current]=\"n === page() ? 'page' : null\"\n                    (click)=\"page.set(n)\"\n                  >\n                    {{ n }}\n                  </button>\n                }\n              </div>\n\n              <button\n                ui-button\n                variant=\"outline\"\n                size=\"sm\"\n                class=\"gap-1 text-xs\"\n                [disabled]=\"page() === totalPages\"\n                (click)=\"page.update((p) => Math.min(totalPages, p + 1))\"\n              >\n                <span>Next</span>\n                <lucide-icon [img]=\"ChevronRight\" class=\"size-4\" />\n              </button>\n            </nav>\n          }\n        </section>\n      </div>\n    </div>\n  `,\n})\nexport class UiProductGridPageComponent {\n  protected readonly Check = Check\n  protected readonly ChevronLeft = ChevronLeft\n  protected readonly ChevronRight = ChevronRight\n  protected readonly Heart = Heart\n  protected readonly RotateCcw = RotateCcw\n  protected readonly ShoppingBag = ShoppingBag\n  protected readonly SlidersHorizontal = SlidersHorizontal\n  protected readonly Star = Star\n  protected readonly X = X\n\n  protected readonly filterCategories = CATEGORIES\n  protected readonly filterBrands = BRANDS\n  protected readonly filterColors = COLORS\n  protected readonly ratingOptions = RATING_OPTIONS\n  protected readonly totalPages = 3\n  protected readonly totalPagesArray = [1, 2, 3]\n  protected readonly Math = Math\n\n  @Input('class') className?: string\n\n  readonly selectedCategories = signal<string[]>([])\n  readonly priceRange = signal<[number, number]>([50, 300])\n  readonly selectedColors = signal<string[]>([])\n  readonly selectedBrands = signal<string[]>([])\n  readonly minRating = signal<number>(0)\n  readonly sortBy = signal<string>('featured')\n  readonly wishlistIds = signal<Set<string>>(new Set(['prod-1', 'prod-5']))\n  readonly addedToCartIds = signal<Set<string>>(new Set())\n  readonly activeProductColor = signal<Record<string, string>>({})\n  readonly page = signal(1)\n  readonly isMobileFilterOpen = signal(false)\n\n  private cartTimers = new Map<string, ReturnType<typeof setTimeout>>()\n\n  readonly hasActiveFilters = computed(\n    () =>\n      this.selectedCategories().length > 0 ||\n      this.selectedBrands().length > 0 ||\n      this.selectedColors().length > 0 ||\n      this.minRating() > 0 ||\n      this.priceRange()[0] > 50 ||\n      this.priceRange()[1] < 300,\n  )\n\n  readonly activeFilterCount = computed(() => {\n    let count = 0\n    if (this.selectedCategories().length > 0) count += this.selectedCategories().length\n    if (this.selectedBrands().length > 0) count += this.selectedBrands().length\n    if (this.selectedColors().length > 0) count += this.selectedColors().length\n    if (this.minRating() > 0) count += 1\n    if (this.priceRange()[0] > 50 || this.priceRange()[1] < 300) count += 1\n    return count\n  })\n\n  readonly filteredProducts = computed(() => {\n    const sort = this.sortBy()\n    return PRODUCTS.filter((product) => {\n      if (this.selectedCategories().length > 0 && !this.selectedCategories().includes(product.categoryId)) {\n        return false\n      }\n      if (product.price < this.priceRange()[0] || product.price > this.priceRange()[1]) {\n        return false\n      }\n      if (this.selectedColors().length > 0) {\n        const hasMatchingColor = product.colors.some((c) => this.selectedColors().includes(c.colorCode))\n        if (!hasMatchingColor) return false\n      }\n      if (this.selectedBrands().length > 0 && !this.selectedBrands().includes(product.brand)) {\n        return false\n      }\n      if (this.minRating() > 0 && product.rating < this.minRating()) {\n        return false\n      }\n      return true\n    }).sort((a, b) => {\n      if (sort === 'price-asc') return a.price - b.price\n      if (sort === 'price-desc') return b.price - a.price\n      if (sort === 'rating-desc') return b.rating - a.rating\n      if (sort === 'newest') return b.createdAt - a.createdAt\n      return a.featuredRank - b.featuredRank\n    })\n  })\n\n  get rootClass(): string {\n    return cn('w-full space-y-8', this.className)\n  }\n\n  categoryLabel(catId: string): string {\n    return CATEGORIES.find((c) => c.id === catId)?.label ?? catId\n  }\n\n  colorLabel(colorId: string): string {\n    return COLORS.find((c) => c.id === colorId)?.label ?? colorId\n  }\n\n  filterColorClass(color: ColorOption): string {\n    return cn(\n      '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 size-7',\n      color.class,\n      this.selectedColors().includes(color.id)\n        ? 'ring-primary ring-offset-background ring-2 ring-offset-2'\n        : 'border-border',\n    )\n  }\n\n  ratingOptionClass(value: number): string {\n    return cn(\n      '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      this.minRating() === value ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground',\n    )\n  }\n\n  wishlistClass(productId: string): string {\n    return cn(\n      '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      this.isWishlisted(productId) && 'text-destructive hover:text-destructive',\n    )\n  }\n\n  cardColorClass(productId: string, color: ProductColor): string {\n    return cn(\n      'size-6 rounded-full transition-opacity hover:opacity-90 focus-visible:outline-none',\n      color.class,\n      this.activeProductColor()[productId] === color.colorCode &&\n        'ring-1.5 ring-primary ring-offset-background ring-offset-1',\n    )\n  }\n\n  toggleCategory(id: string, checked: boolean): void {\n    this.selectedCategories.update((prev) => (checked ? [...prev, id] : prev.filter((item) => item !== id)))\n    this.page.set(1)\n  }\n\n  toggleBrand(id: string, checked: boolean): void {\n    this.selectedBrands.update((prev) => (checked ? [...prev, id] : prev.filter((item) => item !== id)))\n    this.page.set(1)\n  }\n\n  toggleColor(id: string): void {\n    this.selectedColors.update((prev) => (prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]))\n    this.page.set(1)\n  }\n\n  setRating(val: number): void {\n    this.minRating.update((prev) => (prev === val ? 0 : val))\n    this.page.set(1)\n  }\n\n  setPriceRange(value: number[]): void {\n    this.priceRange.set([value[0] ?? 50, value[1] ?? 300])\n    this.page.set(1)\n  }\n\n  setSortBy(value: string): void {\n    this.sortBy.set(value)\n  }\n\n  toggleWishlist(id: string): void {\n    this.wishlistIds.update((prev) => {\n      const next = new Set(prev)\n      if (next.has(id)) next.delete(id)\n      else next.add(id)\n      return next\n    })\n  }\n\n  isWishlisted(id: string): boolean {\n    return this.wishlistIds().has(id)\n  }\n\n  addToCart(id: string): void {\n    clearTimeout(this.cartTimers.get(id))\n    this.addedToCartIds.update((prev) => new Set(prev).add(id))\n    this.cartTimers.set(\n      id,\n      setTimeout(() => {\n        this.addedToCartIds.update((prev) => {\n          const next = new Set(prev)\n          next.delete(id)\n          return next\n        })\n        this.cartTimers.delete(id)\n      }, 1600),\n    )\n  }\n\n  isAddedToCart(id: string): boolean {\n    return this.addedToCartIds().has(id)\n  }\n\n  selectProductColor(productId: string, colorCode: string): void {\n    this.activeProductColor.update((prev) => ({ ...prev, [productId]: colorCode }))\n  }\n\n  resetFilters(): void {\n    this.selectedCategories.set([])\n    this.priceRange.set([50, 300])\n    this.selectedColors.set([])\n    this.selectedBrands.set([])\n    this.minRating.set(0)\n    this.page.set(1)\n  }\n\n  getCategoryCount(catId: string): number {\n    return PRODUCTS.filter((p) => p.categoryId === catId).length\n  }\n\n  getBrandCount(brand: string): number {\n    return PRODUCTS.filter((p) => p.brand === brand).length\n  }\n}\n",
      "type": "registry:page",
      "target": "~/src/app/components/blocks/product-grid-page/product-grid-page.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/product-grid-page/index.ts",
      "content": "export {\n  UiProductGridPageComponent,\n  CATEGORIES,\n  BRANDS,\n  COLORS,\n  RATING_OPTIONS,\n  type CategoryOption,\n  type BrandOption,\n  type ColorOption,\n} from './product-grid-page.component'\n",
      "type": "registry:page",
      "target": "~/src/app/components/blocks/product-grid-page/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/breadcrumb.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/checkbox.json",
    "https://uipkge.dev/r/angular/select.json",
    "https://uipkge.dev/r/angular/separator.json",
    "https://uipkge.dev/r/angular/sheet.json",
    "https://uipkge.dev/r/angular/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"
  ]
}