{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "product-grid-page",
  "title": "Product Grid Page",
  "type": "registry:page",
  "files": [
    {
      "path": "packages/registry-svelte/blocks/product-grid-page/ProductGridPage.svelte",
      "content": "<script lang=\"ts\">\n  import {\n    Check,\n    ChevronLeft,\n    ChevronRight,\n    Heart,\n    RotateCcw,\n    ShoppingBag,\n    SlidersHorizontal,\n    Star,\n    X,\n  } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n  import { Badge } from '$lib/components/ui/badge'\n  import {\n    Breadcrumb,\n    BreadcrumbItem,\n    BreadcrumbLink,\n    BreadcrumbList,\n    BreadcrumbPage,\n    BreadcrumbSeparator,\n  } from '$lib/components/ui/breadcrumb'\n  import { Button } from '$lib/components/ui/button'\n  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '$lib/components/ui/select'\n  import { Separator } from '$lib/components/ui/separator'\n  import {\n    Sheet,\n    SheetContent,\n    SheetDescription,\n    SheetHeader,\n    SheetTitle,\n    SheetTrigger,\n  } from '$lib/components/ui/sheet'\n  import ProductFilterSidebar from './ProductFilterSidebar.svelte'\n  import { CATEGORIES, COLORS } from './product-filter-data'\n\n  interface ProductColor {\n    id: string\n    name: string\n    class: string\n    colorCode: string\n  }\n\n  interface 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\n  interface Props {\n    class?: string\n  }\n\n  let { class: className }: Props = $props()\n\n  const 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  let selectedCategories: string[] = $state([])\n  let priceRange: [number, number] = $state([50, 300])\n  let selectedColors: string[] = $state([])\n  let selectedBrands: string[] = $state([])\n  let minRating = $state(0)\n  let sortBy = $state('featured')\n  let wishlistIds: Set<string> = $state(new Set(['prod-1', 'prod-5']))\n  let addedToCartIds: Set<string> = $state(new Set())\n  let activeProductColor: Record<string, string> = $state({})\n  let page = $state(1)\n  const totalPages = 3\n  let isMobileFilterOpen = $state(false)\n\n  function toggleCategory(id: string, checked: boolean) {\n    if (checked) {\n      selectedCategories = [...selectedCategories, id]\n    } else {\n      selectedCategories = selectedCategories.filter((item) => item !== id)\n    }\n    page = 1\n  }\n\n  function toggleBrand(id: string, checked: boolean) {\n    if (checked) {\n      selectedBrands = [...selectedBrands, id]\n    } else {\n      selectedBrands = selectedBrands.filter((item) => item !== id)\n    }\n    page = 1\n  }\n\n  function toggleColor(id: string) {\n    if (selectedColors.includes(id)) {\n      selectedColors = selectedColors.filter((item) => item !== id)\n    } else {\n      selectedColors = [...selectedColors, id]\n    }\n    page = 1\n  }\n\n  function setRating(val: number) {\n    if (minRating === val) {\n      minRating = 0\n    } else {\n      minRating = val\n    }\n    page = 1\n  }\n\n  function toggleWishlist(id: string) {\n    const next = new Set(wishlistIds)\n    if (next.has(id)) {\n      next.delete(id)\n    } else {\n      next.add(id)\n    }\n    wishlistIds = next\n  }\n\n  function isWishlisted(id: string) {\n    return wishlistIds.has(id)\n  }\n\n  function addToCart(id: string) {\n    const next = new Set(addedToCartIds)\n    next.add(id)\n    addedToCartIds = next\n    setTimeout(() => {\n      const updated = new Set(addedToCartIds)\n      updated.delete(id)\n      addedToCartIds = updated\n    }, 1600)\n  }\n\n  function isAddedToCart(id: string) {\n    return addedToCartIds.has(id)\n  }\n\n  function selectProductColor(productId: string, colorCode: string) {\n    activeProductColor = {\n      ...activeProductColor,\n      [productId]: colorCode,\n    }\n  }\n\n  function resetFilters() {\n    selectedCategories = []\n    priceRange = [50, 300]\n    selectedColors = []\n    selectedBrands = []\n    minRating = 0\n    page = 1\n  }\n\n  const hasActiveFilters = $derived(\n    selectedCategories.length > 0 ||\n      selectedBrands.length > 0 ||\n      selectedColors.length > 0 ||\n      minRating > 0 ||\n      priceRange[0] > 50 ||\n      priceRange[1] < 300,\n  )\n\n  const activeFilterCount = $derived.by(() => {\n    let count = 0\n    if (selectedCategories.length > 0) count += selectedCategories.length\n    if (selectedBrands.length > 0) count += selectedBrands.length\n    if (selectedColors.length > 0) count += selectedColors.length\n    if (minRating > 0) count += 1\n    if (priceRange[0] > 50 || priceRange[1] < 300) count += 1\n    return count\n  })\n\n  function getCategoryCount(catId: string) {\n    return PRODUCTS.filter((p) => p.categoryId === catId).length\n  }\n\n  function getBrandCount(brand: string) {\n    return PRODUCTS.filter((p) => p.brand === brand).length\n  }\n\n  const filteredProducts = $derived.by(() => {\n    return PRODUCTS.filter((product) => {\n      if (selectedCategories.length > 0 && !selectedCategories.includes(product.categoryId)) {\n        return false\n      }\n      if (product.price < priceRange[0] || product.price > priceRange[1]) {\n        return false\n      }\n      if (selectedColors.length > 0) {\n        const hasMatchingColor = product.colors.some((c) => selectedColors.includes(c.colorCode))\n        if (!hasMatchingColor) return false\n      }\n      if (selectedBrands.length > 0 && !selectedBrands.includes(product.brand)) {\n        return false\n      }\n      if (minRating > 0 && product.rating < minRating) {\n        return false\n      }\n      return true\n    }).sort((a, b) => {\n      if (sortBy === 'price-asc') return a.price - b.price\n      if (sortBy === 'price-desc') return b.price - a.price\n      if (sortBy === 'rating-desc') return b.rating - a.rating\n      if (sortBy === 'newest') return b.createdAt - a.createdAt\n      return a.featuredRank - b.featuredRank\n    })\n  })\n\n  const pageNumbers = $derived(Array.from({ length: totalPages }, (_, i) => i + 1))\n</script>\n\n<div data-slot=\"product-grid-page\" class={cn('w-full space-y-8', className)}>\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 bind:open={isMobileFilterOpen}>\n        <SheetTrigger>\n          {#snippet child({ props })}\n            <Button variant=\"outline\" size=\"sm\" class=\"gap-2 md:hidden\" {...props}>\n              <SlidersHorizontal class=\"size-4\" />\n              <span>Filters</span>\n              {#if activeFilterCount > 0}\n                <Badge variant=\"secondary\" class=\"ml-1 px-1.5 py-0 text-xs\">\n                  {activeFilterCount}\n                </Badge>\n              {/if}\n            </Button>\n          {/snippet}\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              {#if hasActiveFilters}\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  class=\"text-muted-foreground hover:text-foreground h-auto p-0 text-xs font-normal\"\n                  onclick={resetFilters}\n                >\n                  Reset all\n                </Button>\n              {/if}\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              isMobile\n              {selectedCategories}\n              {priceRange}\n              {selectedColors}\n              {selectedBrands}\n              {minRating}\n              {getCategoryCount}\n              {getBrandCount}\n              onToggleCategory={toggleCategory}\n              onPriceRangeChange={(v) => (priceRange = v)}\n              onToggleColor={toggleColor}\n              onToggleBrand={toggleBrand}\n              onSelectRating={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 bind:value={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        {#if hasActiveFilters}\n          <Button\n            variant=\"ghost\"\n            size=\"sm\"\n            class=\"text-muted-foreground hover:text-foreground h-auto p-0 text-xs font-normal\"\n            onclick={resetFilters}\n          >\n            Reset all\n          </Button>\n        {/if}\n      </div>\n\n      <Separator />\n\n      <ProductFilterSidebar\n        {selectedCategories}\n        {priceRange}\n        {selectedColors}\n        {selectedBrands}\n        {minRating}\n        {getCategoryCount}\n        {getBrandCount}\n        onToggleCategory={toggleCategory}\n        onPriceRangeChange={(v) => (priceRange = v)}\n        onToggleColor={toggleColor}\n        onToggleBrand={toggleBrand}\n        onSelectRating={setRating}\n      />\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          {#each selectedCategories as catId (catId)}\n            <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                onclick={() => toggleCategory(catId, false)}\n              >\n                <X class=\"size-3\" />\n              </button>\n            </Badge>\n          {/each}\n\n          {#each selectedBrands as brand (brand)}\n            <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                onclick={() => toggleBrand(brand, false)}\n              >\n                <X class=\"size-3\" />\n              </button>\n            </Badge>\n          {/each}\n\n          {#each selectedColors as colorId (colorId)}\n            <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                onclick={() => toggleColor(colorId)}\n              >\n                <X class=\"size-3\" />\n              </button>\n            </Badge>\n          {/each}\n\n          {#if minRating > 0}\n            <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                onclick={() => (minRating = 0)}\n              >\n                <X class=\"size-3\" />\n              </button>\n            </Badge>\n          {/if}\n\n          {#if priceRange[0] > 50 || priceRange[1] < 300}\n            <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                onclick={() => (priceRange = [50, 300])}\n              >\n                <X class=\"size-3\" />\n              </button>\n            </Badge>\n          {/if}\n\n          <Button\n            variant=\"link\"\n            size=\"sm\"\n            class=\"text-muted-foreground hover:text-foreground h-auto px-1 text-xs\"\n            onclick={resetFilters}\n          >\n            Clear all\n          </Button>\n        </div>\n      {/if}\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            <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\" onclick={resetFilters}>\n            <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          {#each filteredProducts as product (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              <!-- 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                {#if product.badge}\n                  <div 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                {/if}\n\n                <!-- Top Right Wishlist Heart -->\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                    isWishlisted(product.id) && 'text-destructive hover:text-destructive',\n                  )}\n                  onclick={(e) => {\n                    e.stopPropagation()\n                    toggleWishlist(product.id)\n                  }}\n                >\n                  <Heart\n                    class={cn(\n                      'size-4 transition-transform active:scale-90',\n                      isWishlisted(product.id) && 'fill-current',\n                    )}\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                    onclick={(e) => {\n                      e.stopPropagation()\n                      addToCart(product.id)\n                    }}\n                  >\n                    {#if isAddedToCart(product.id)}\n                      <Check class=\"size-3.5\" />\n                    {:else}\n                      <ShoppingBag class=\"size-3.5\" />\n                    {/if}\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                    {#each product.colors as color (color.id)}\n                      <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                          activeProductColor[product.id] === color.colorCode\n                            ? 'ring-1.5 ring-primary ring-offset-background ring-offset-1'\n                            : '',\n                        )}\n                        onclick={(e) => {\n                          e.stopPropagation()\n                          selectProductColor(product.id, color.colorCode)\n                        }}\n                      >\n                        <span class=\"sr-only\">{color.name}</span>\n                      </button>\n                    {/each}\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                  {#if product.originalPrice}\n                    <span class=\"text-muted-foreground text-xs line-through\">\n                      ${product.originalPrice.toFixed(2)}\n                    </span>\n                  {/if}\n                  {#if product.originalPrice}\n                    <span class=\"text-success text-xs font-medium\">\n                      Save ${(product.originalPrice - product.price).toFixed(2)}\n                    </span>\n                  {/if}\n                </div>\n              </div>\n            </div>\n          {/each}\n        </div>\n      {/if}\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            variant=\"outline\"\n            size=\"sm\"\n            class=\"gap-1 text-xs\"\n            disabled={page === 1}\n            onclick={() => (page -= 1)}\n          >\n            <ChevronLeft class=\"size-4\" />\n            <span>Previous</span>\n          </Button>\n\n          <div class=\"flex items-center gap-1\">\n            {#each pageNumbers as n (n)}\n              <Button\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                onclick={() => (page = n)}\n              >\n                {n}\n              </Button>\n            {/each}\n          </div>\n\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            class=\"gap-1 text-xs\"\n            disabled={page === totalPages}\n            onclick={() => (page += 1)}\n          >\n            <span>Next</span>\n            <ChevronRight class=\"size-4\" />\n          </Button>\n        </nav>\n      {/if}\n    </section>\n  </div>\n</div>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/product-grid-page/ProductGridPage.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/product-grid-page/ProductFilterSidebar.svelte",
      "content": "<script lang=\"ts\">\n  import { Check, Star } from '@lucide/svelte'\n  import { Checkbox } from '$lib/components/ui/checkbox'\n  import { Separator } from '$lib/components/ui/separator'\n  import { Slider } from '$lib/components/ui/slider'\n  import { cn } from '$lib/utils'\n\n  import { BRANDS, CATEGORIES, COLORS, RATING_OPTIONS } from './product-filter-data'\n\n  interface Props {\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\n  let {\n    isMobile = false,\n    selectedCategories,\n    priceRange,\n    selectedColors,\n    selectedBrands,\n    minRating,\n    getCategoryCount,\n    getBrandCount,\n    onToggleCategory,\n    onPriceRangeChange,\n    onToggleColor,\n    onToggleBrand,\n    onSelectRating,\n  }: Props = $props()\n\n  // svelte-ignore state_referenced_locally: isMobile is set once by the parent layout, never toggled.\n  const idPrefix = isMobile ? 'mobile-' : ''\n\n  // Slider reports live drags through bind:value only, so mirror the\n  // controlled prop locally: drags flow up via onPriceRangeChange, parent\n  // resets (Reset all) flow back down through the first effect.\n  // svelte-ignore state_referenced_locally: one-shot seed; the effect below re-syncs parent resets.\n  let rangeValue: [number, number] = $state(priceRange)\n  $effect(() => {\n    if (rangeValue[0] !== priceRange[0] || rangeValue[1] !== priceRange[1]) rangeValue = priceRange\n  })\n  $effect(() => {\n    onPriceRangeChange(rangeValue)\n  })\n</script>\n\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      {#each CATEGORIES as cat (cat.id)}\n        <div 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              checked={selectedCategories.includes(cat.id)}\n              onCheckedChange={(v) => onToggleCategory(cat.id, v === 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      {/each}\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        ${rangeValue[0]} – ${rangeValue[1]}\n      </span>\n    </div>\n    <Slider bind:value={rangeValue} min={50} max={300} step={5} range class=\"py-2\" />\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      {#each COLORS as color (color.id)}\n        <button\n          type=\"button\"\n          title={color.label}\n          class={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',\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          onclick={() => onToggleColor(color.id)}\n        >\n          {#if selectedColors.includes(color.id)}\n            <Check\n              class={cn(isMobile ? 'size-4' : 'size-3.5', color.id === 'white' ? 'text-zinc-950' : 'text-white')}\n            />\n          {/if}\n          <span class=\"sr-only\">{color.label}</span>\n        </button>\n      {/each}\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      {#each BRANDS as b (b.id)}\n        <div 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              checked={selectedBrands.includes(b.id)}\n              onCheckedChange={(v) => onToggleBrand(b.id, v === 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      {/each}\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      {#each RATING_OPTIONS as opt (opt.value)}\n        <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            minRating === opt.value ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground',\n          )}\n          onclick={() => 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          {#if minRating === opt.value}\n            <Check class=\"text-primary size-3.5\" />\n          {/if}\n        </button>\n      {/each}\n    </div>\n  </div>\n</div>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/product-grid-page/ProductFilterSidebar.svelte"
    },
    {
      "path": "packages/registry-svelte/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": "~/src/lib/components/blocks/product-grid-page/product-filter-data.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/badge.json",
    "https://uipkge.dev/r/svelte/breadcrumb.json",
    "https://uipkge.dev/r/svelte/button.json",
    "https://uipkge.dev/r/svelte/checkbox.json",
    "https://uipkge.dev/r/svelte/select.json",
    "https://uipkge.dev/r/svelte/separator.json",
    "https://uipkge.dev/r/svelte/sheet.json",
    "https://uipkge.dev/r/svelte/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"
  ]
}