{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "product-grid-page",
  "title": "Product Grid Page",
  "type": "registry:page",
  "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"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/product-grid-page/product-grid-page.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport {\n  Check,\n  ChevronLeft,\n  ChevronRight,\n  Heart,\n  RotateCcw,\n  ShoppingBag,\n  SlidersHorizontal,\n  Star,\n  X,\n} from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect imports: register the composed elements for standalone use\n// (consumers who install only this block still get working primitives).\nimport '../../components/badge/badge'\nimport '../../components/breadcrumb/breadcrumb'\nimport '../../components/button/button'\nimport '../../components/select/select'\nimport type { UipSelect } from '../../components/select/select'\nimport '../../components/separator/separator'\nimport '../../components/sheet/sheet'\nimport '../../components/checkbox/checkbox'\nimport '../../components/slider/slider'\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\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\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-6bf12165a8df?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/**\n * <uip-product-grid-page> — the registry ProductGridPage block as a web component.\n *\n * Class strings and catalog data are React's verbatim, including the filter\n * sidebar (ported as an internal section — React's ProductFilterSidebar is a\n * file of this same block, not a separate install unit). React state becomes\n * { state: true } fields. The block takes no props and fires no events;\n * faceted filtering, sorting, wishlist, quick-add, and pagination are internal.\n */\nexport class UipProductGridPage extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    selectedCategories: { state: true },\n    priceRange: { state: true },\n    selectedColors: { state: true },\n    selectedBrands: { state: true },\n    minRating: { state: true },\n    sortBy: { state: true },\n    wishlistIds: { state: true },\n    addedToCartIds: { state: true },\n    activeProductColors: { state: true },\n    page: { state: true },\n    isMobileFilterOpen: { state: true },\n  }\n\n  private selectedCategories: string[] = []\n  private priceRange: number[] = [50, 300]\n  private selectedColors: string[] = []\n  private selectedBrands: string[] = []\n  private minRating = 0\n  private sortBy = 'featured'\n  private wishlistIds: string[] = ['prod-1', 'prod-5']\n  private addedToCartIds: string[] = []\n  private activeProductColors: Record<string, string> = {}\n  private page = 1\n  private isMobileFilterOpen = false\n  private cartTimer?: number\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'product-grid-page')\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    if (this.cartTimer) window.clearTimeout(this.cartTimer)\n  }\n\n  private toggleCategory(id: string, checked: boolean) {\n    this.selectedCategories = checked\n      ? [...this.selectedCategories, id]\n      : this.selectedCategories.filter((item) => item !== id)\n    this.page = 1\n  }\n\n  private toggleBrand(id: string, checked: boolean) {\n    this.selectedBrands = checked ? [...this.selectedBrands, id] : this.selectedBrands.filter((item) => item !== id)\n    this.page = 1\n  }\n\n  private toggleColor(id: string) {\n    this.selectedColors = this.selectedColors.includes(id)\n      ? this.selectedColors.filter((item) => item !== id)\n      : [...this.selectedColors, id]\n    this.page = 1\n  }\n\n  private setRating(val: number) {\n    this.minRating = this.minRating === val ? 0 : val\n    this.page = 1\n  }\n\n  private toggleWishlist(id: string) {\n    this.wishlistIds = this.wishlistIds.includes(id)\n      ? this.wishlistIds.filter((item) => item !== id)\n      : [...this.wishlistIds, id]\n  }\n\n  private addToCart(id: string) {\n    if (this.addedToCartIds.includes(id)) return\n    this.addedToCartIds = [...this.addedToCartIds, id]\n    if (this.cartTimer) window.clearTimeout(this.cartTimer)\n    this.cartTimer = window.setTimeout(() => {\n      this.addedToCartIds = this.addedToCartIds.filter((item) => item !== id)\n    }, 1600)\n  }\n\n  private selectProductColor(productId: string, colorCode: string) {\n    this.activeProductColors = { ...this.activeProductColors, [productId]: colorCode }\n  }\n\n  private resetFilters() {\n    this.selectedCategories = []\n    this.priceRange = [50, 300]\n    this.selectedColors = []\n    this.selectedBrands = []\n    this.minRating = 0\n    this.page = 1\n  }\n\n  private get hasActiveFilters() {\n    return (\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\n  private get activeFilterCount() {\n    return (\n      this.selectedCategories.length +\n      this.selectedBrands.length +\n      this.selectedColors.length +\n      (this.minRating > 0 ? 1 : 0) +\n      (this.priceRange[0]! > 50 || this.priceRange[1]! < 300 ? 1 : 0)\n    )\n  }\n\n  private get filteredProducts(): Product[] {\n    return PRODUCTS.filter((product) => {\n      if (this.selectedCategories.length > 0 && !this.selectedCategories.includes(product.categoryId)) return false\n      if (product.price < this.priceRange[0]! || product.price > this.priceRange[1]!) return false\n      if (this.selectedColors.length > 0) {\n        if (!product.colors.some((c) => this.selectedColors.includes(c.colorCode))) return false\n      }\n      if (this.selectedBrands.length > 0 && !this.selectedBrands.includes(product.brand)) return false\n      if (this.minRating > 0 && product.rating < this.minRating) return false\n      return true\n    }).sort((a, b) => {\n      if (this.sortBy === 'price-asc') return a.price - b.price\n      if (this.sortBy === 'price-desc') return b.price - a.price\n      if (this.sortBy === 'rating-desc') return b.rating - a.rating\n      if (this.sortBy === 'newest') return b.createdAt - a.createdAt\n      return a.featuredRank - b.featuredRank\n    })\n  }\n\n  private renderFilterSidebar(idPrefix: string) {\n    return html`<div class=\"space-y-6\">\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          ${CATEGORIES.map(\n            (cat) => html`<div class=\"flex items-center justify-between\">\n              <label for=${`${idPrefix}cat-${cat.id}`} class=\"text-foreground flex cursor-pointer items-center gap-2 text-sm\">\n                <uip-checkbox\n                  id=${`${idPrefix}cat-${cat.id}`}\n                  .checked=${this.selectedCategories.includes(cat.id)}\n                  @checked-change=${(e: CustomEvent<{ checked: boolean }>) =>\n                    this.toggleCategory(cat.id, e.detail.checked)}\n                ></uip-checkbox>\n                <span>${cat.label}</span>\n              </label>\n              <span class=\"text-muted-foreground text-xs tabular-nums\"\n                >${PRODUCTS.filter((p) => p.categoryId === cat.id).length}</span\n              >\n            </div>`,\n          )}\n        </div>\n      </div>\n\n      <uip-separator></uip-separator>\n\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\">$${this.priceRange[0]} – $${this.priceRange[1]}</span>\n        </div>\n        <uip-slider\n          .value=${this.priceRange}\n          range\n          min=\"50\"\n          max=\"300\"\n          step=\"5\"\n          class=\"py-2\"\n          @value-change=${(e: CustomEvent<number[]>) => {\n            this.priceRange = e.detail\n            this.page = 1\n          }}\n        ></uip-slider>\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      <uip-separator></uip-separator>\n\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          ${COLORS.map(\n            (color) => html`<button\n              type=\"button\"\n              title=${color.label}\n              class=${cn(\n                'focus-visible:ring-ring relative flex size-7 items-center justify-center rounded-full border transition-transform hover:scale-110 focus-visible:ring-2 focus-visible:outline-none',\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              @click=${() => this.toggleColor(color.id)}\n            >\n              ${this.selectedColors.includes(color.id)\n                ? html`${icon(Check, 'check', cn('size-3.5', color.id === 'white' ? 'text-zinc-950' : 'text-white'))}`\n                : nothing}\n              <span class=\"sr-only\">${color.label}</span>\n            </button>`,\n          )}\n        </div>\n      </div>\n\n      <uip-separator></uip-separator>\n\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          ${BRANDS.map(\n            (b) => html`<div class=\"flex items-center justify-between\">\n              <label for=${`${idPrefix}brand-${b.id}`} class=\"text-foreground flex cursor-pointer items-center gap-2 text-sm\">\n                <uip-checkbox\n                  id=${`${idPrefix}brand-${b.id}`}\n                  .checked=${this.selectedBrands.includes(b.id)}\n                  @checked-change=${(e: CustomEvent<{ checked: boolean }>) => this.toggleBrand(b.id, e.detail.checked)}\n                ></uip-checkbox>\n                <span>${b.label}</span>\n              </label>\n              <span class=\"text-muted-foreground text-xs tabular-nums\"\n                >${PRODUCTS.filter((p) => p.brand === b.id).length}</span\n              >\n            </div>`,\n          )}\n        </div>\n      </div>\n\n      <uip-separator></uip-separator>\n\n      <div class=\"space-y-3\">\n        <h3 class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">Customer Rating</h3>\n        <div class=\"space-y-1.5\">\n          ${RATING_OPTIONS.map(\n            (opt) => html`<button\n              type=\"button\"\n              class=${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 === opt.value ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground',\n              )}\n              @click=${() => this.setRating(opt.value)}\n            >\n              <div class=\"flex items-center gap-1.5\">\n                ${icon(Star, 'star', 'fill-warning text-warning size-3.5')}\n                <span>${opt.label}</span>\n              </div>\n              ${this.minRating === opt.value ? icon(Check, 'check', 'text-primary size-3.5') : nothing}\n            </button>`,\n          )}\n        </div>\n      </div>\n    </div>`\n  }\n\n  render() {\n    const filteredProducts = this.filteredProducts\n    const totalPages = 3\n    return html`\n      <div data-slot=\"product-grid-page\" class=\"w-full space-y-8\">\n        <uip-breadcrumb>\n          <uip-breadcrumb-item>\n            <uip-breadcrumb-link href=\"#\" class=\"inline-flex min-h-6 items-center\">Home</uip-breadcrumb-link>\n          </uip-breadcrumb-item>\n          <uip-breadcrumb-separator></uip-breadcrumb-separator>\n          <uip-breadcrumb-item>\n            <uip-breadcrumb-link href=\"#\" class=\"inline-flex min-h-6 items-center\">Shop</uip-breadcrumb-link>\n          </uip-breadcrumb-item>\n          <uip-breadcrumb-separator></uip-breadcrumb-separator>\n          <uip-breadcrumb-item>\n            <uip-breadcrumb-page>Footwear</uip-breadcrumb-page>\n          </uip-breadcrumb-item>\n        </uip-breadcrumb>\n\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              <uip-badge variant=\"secondary\" class=\"font-normal tabular-nums\">${filteredProducts.length} items</uip-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            <uip-button\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"gap-2 md:hidden\"\n              @click=${() => {\n                this.isMobileFilterOpen = true\n              }}\n            >\n              ${icon(SlidersHorizontal, 'sliders-horizontal', 'size-4')}\n              <span>Filters</span>\n              ${this.activeFilterCount > 0\n                ? html`<uip-badge variant=\"secondary\" class=\"ml-1 px-1.5 py-0 text-xs\">${this.activeFilterCount}</uip-badge>`\n                : nothing}\n            </uip-button>\n\n            <div class=\"flex items-center gap-2\">\n              <span class=\"text-muted-foreground hidden text-xs sm:inline-block\">Sort by:</span>\n              <uip-select\n                class=\"w-44 text-xs sm:text-sm\"\n                .value=${this.sortBy}\n                @change=${(e: Event) => {\n                  this.sortBy = (e.target as UipSelect).value\n                }}\n              >\n                <option value=\"featured\">Featured</option>\n                <option value=\"price-asc\">Price: Low to High</option>\n                <option value=\"price-desc\">Price: High to Low</option>\n                <option value=\"rating-desc\">Highest Rated</option>\n                <option value=\"newest\">Newest</option>\n              </uip-select>\n            </div>\n          </div>\n        </div>\n\n        <div class=\"grid grid-cols-1 gap-8 md:grid-cols-[240px_1fr] lg:grid-cols-[260px_1fr]\">\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              ${this.hasActiveFilters\n                ? html`<uip-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=${() => this.resetFilters()}\n                  >\n                    Reset all\n                  </uip-button>`\n                : nothing}\n            </div>\n            <uip-separator></uip-separator>\n            ${this.renderFilterSidebar('')}\n          </aside>\n\n          <section class=\"min-w-0 space-y-6\">\n            ${this.hasActiveFilters\n              ? html`<div class=\"flex flex-wrap items-center gap-2\">\n                  ${this.selectedCategories.map(\n                    (catId) => html`<uip-badge 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=${() => this.toggleCategory(catId, false)}\n                      >\n                        ${icon(X, 'x', 'size-3')}\n                      </button>\n                    </uip-badge>`,\n                  )}\n                  ${this.selectedBrands.map(\n                    (brand) => html`<uip-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=${() => this.toggleBrand(brand, false)}\n                      >\n                        ${icon(X, 'x', 'size-3')}\n                      </button>\n                    </uip-badge>`,\n                  )}\n                  ${this.selectedColors.map(\n                    (colorId) => html`<uip-badge 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=${() => this.toggleColor(colorId)}\n                      >\n                        ${icon(X, 'x', 'size-3')}\n                      </button>\n                    </uip-badge>`,\n                  )}\n                  ${this.minRating > 0\n                    ? html`<uip-badge variant=\"secondary\" class=\"gap-1.5 py-1 text-xs\">\n                        <span>Rating: ${this.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=${() => {\n                            this.minRating = 0\n                          }}\n                        >\n                          ${icon(X, 'x', 'size-3')}\n                        </button>\n                      </uip-badge>`\n                    : nothing}\n                  ${this.priceRange[0]! > 50 || this.priceRange[1]! < 300\n                    ? html`<uip-badge variant=\"secondary\" class=\"gap-1.5 py-1 text-xs\">\n                        <span>Price: $${this.priceRange[0]} - $${this.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=${() => {\n                            this.priceRange = [50, 300]\n                          }}\n                        >\n                          ${icon(X, 'x', 'size-3')}\n                        </button>\n                      </uip-badge>`\n                    : nothing}\n                  <uip-button\n                    variant=\"link\"\n                    size=\"sm\"\n                    class=\"text-muted-foreground hover:text-foreground h-auto px-1 text-xs\"\n                    @click=${() => this.resetFilters()}\n                  >\n                    Clear all\n                  </uip-button>\n                </div>`\n              : nothing}\n\n            ${filteredProducts.length === 0\n              ? html`<div class=\"border-border flex flex-col items-center justify-center rounded-xl border border-dashed py-16 text-center\">\n                  <div class=\"bg-muted text-muted-foreground flex size-12 items-center justify-center rounded-full\">\n                    ${icon(RotateCcw, 'rotate-ccw', '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                  <uip-button variant=\"outline\" size=\"sm\" class=\"mt-4 gap-2\" @click=${() => this.resetFilters()}>\n                    ${icon(RotateCcw, 'rotate-ccw', 'size-3.5')}\n                    <span>Reset filters</span>\n                  </uip-button>\n                </div>`\n              : html`<div class=\"grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3\">\n                  ${filteredProducts.map(\n                    (product) => html`<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                        ${product.badge\n                          ? html`<div class=\"absolute top-2.5 left-2.5 z-10\">\n                              <uip-badge variant=${product.badge.variant} class=\"text-xs shadow-xs\"\n                                >${product.badge.label}</uip-badge\n                              >\n                            </div>`\n                          : nothing}\n                        <button\n                          type=\"button\"\n                          aria-label=\"Toggle wishlist\"\n                          class=${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.wishlistIds.includes(product.id) && 'text-destructive hover:text-destructive',\n                          )}\n                          @click=${(e: Event) => {\n                            e.stopPropagation()\n                            this.toggleWishlist(product.id)\n                          }}\n                        >\n                          ${icon(\n                            Heart,\n                            'heart',\n                            cn('size-4 transition-transform active:scale-90', this.wishlistIds.includes(product.id) && 'fill-current'),\n                          )}\n                        </button>\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                          <uip-button\n                            size=\"sm\"\n                            class=\"w-full gap-1.5 text-xs font-medium shadow-sm\"\n                            variant=\"default\"\n                            @click=${(e: Event) => {\n                              e.stopPropagation()\n                              this.addToCart(product.id)\n                            }}\n                          >\n                            ${this.addedToCartIds.includes(product.id)\n                              ? html`${icon(Check, 'check', 'size-3.5')}<span>Added to Cart</span>`\n                              : html`${icon(ShoppingBag, 'shopping-bag', 'size-3.5')}<span>Quick Add</span>`}\n                          </uip-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                            ${product.colors.map(\n                              (color) => html`<button\n                                type=\"button\"\n                                title=${color.name}\n                                class=${cn(\n                                  'size-6 rounded-full transition-opacity hover:opacity-90 focus-visible:outline-none',\n                                  color.class,\n                                  this.activeProductColors[product.id] === color.colorCode\n                                    ? 'ring-1.5 ring-primary ring-offset-background ring-offset-1'\n                                    : '',\n                                )}\n                                @click=${(e: Event) => {\n                                  e.stopPropagation()\n                                  this.selectProductColor(product.id, color.colorCode)\n                                }}\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                          <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                              ${icon(Star, 'star', '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                          <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                        <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                          ${product.originalPrice\n                            ? html`<span class=\"text-muted-foreground text-xs line-through\"\n                                >$${product.originalPrice.toFixed(2)}</span\n                              >`\n                            : nothing}\n                          ${product.originalPrice\n                            ? html`<span class=\"text-success text-xs font-medium\"\n                                >Save $${(product.originalPrice - product.price).toFixed(2)}</span\n                              >`\n                            : nothing}\n                        </div>\n                      </div>\n                    </div>`,\n                  )}\n                </div>`}\n\n            ${filteredProducts.length > 0\n              ? html`<nav class=\"border-border flex items-center justify-between border-t pt-6\" aria-label=\"Pagination\">\n                  <uip-button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    class=\"gap-1 text-xs\"\n                    ?disabled=${this.page === 1}\n                    @click=${() => {\n                      this.page = Math.max(1, this.page - 1)\n                    }}\n                  >\n                    ${icon(ChevronLeft, 'chevron-left', 'size-4')}\n                    <span>Previous</span>\n                  </uip-button>\n                  <div class=\"flex items-center gap-1\">\n                    ${[1, 2, 3].map(\n                      (n) => html`<uip-button\n                        variant=${n === this.page ? 'outline' : 'ghost'}\n                        size=\"sm\"\n                        class=\"size-8 p-0 text-xs tabular-nums\"\n                        aria-current=${n === this.page ? 'page' : nothing}\n                        @click=${() => {\n                          this.page = n\n                        }}\n                      >\n                        ${n}\n                      </uip-button>`,\n                    )}\n                  </div>\n                  <uip-button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    class=\"gap-1 text-xs\"\n                    ?disabled=${this.page === totalPages}\n                    @click=${() => {\n                      this.page = Math.min(totalPages, this.page + 1)\n                    }}\n                  >\n                    <span>Next</span>\n                    ${icon(ChevronRight, 'chevron-right', 'size-4')}\n                  </uip-button>\n                </nav>`\n              : nothing}\n          </section>\n        </div>\n\n        <uip-sheet\n          side=\"left\"\n          ?open=${this.isMobileFilterOpen}\n          heading=\"Filters\"\n          description=\"Narrow down footwear by category, price, and specs.\"\n          @open-change=${(e: CustomEvent<{ open: boolean }>) => {\n            this.isMobileFilterOpen = e.detail.open\n          }}\n        >\n          <uip-sheet-body>\n            <div class=\"mt-6\">\n              <div class=\"flex items-center justify-between\">\n                <span class=\"text-foreground text-sm font-semibold\">Filters</span>\n                ${this.hasActiveFilters\n                  ? html`<uip-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=${() => this.resetFilters()}\n                    >\n                      Reset all\n                    </uip-button>`\n                  : nothing}\n              </div>\n              <div class=\"mt-4\">${this.renderFilterSidebar('mobile-')}</div>\n            </div>\n          </uip-sheet-body>\n        </uip-sheet>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-product-grid-page') || customElements.define('uip-product-grid-page', UipProductGridPage)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-product-grid-page': UipProductGridPage\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/product-grid-page/product-grid-page.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/badge.json",
    "https://uipkge.dev/r/lit/breadcrumb.json",
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/checkbox.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/select.json",
    "https://uipkge.dev/r/lit/separator.json",
    "https://uipkge.dev/r/lit/sheet.json",
    "https://uipkge.dev/r/lit/slider.json"
  ]
}
