# Product List

> Product list with list, grid, carousel, and picker variants.

A shadcn/ui block from the [@manifest](https://ui.manifest.build) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/manifest/product-list).

- Page, with a live preview: https://shadcn.exchange/r/manifest/product-list
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/manifest/product-list-light-5d2493586d12f221.png
- Registry item: https://ui.manifest.build/r/product-list.json
- Categories: list
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.manifest.build/r/product-list.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `https://ui.manifest.build/r/manifest-types.json`

## Files

### `registry/list/product-list.tsx`

````tsx
'use client'

import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { Check, ChevronLeft, ChevronRight, ShoppingCart, Star } from 'lucide-react'
import { useCallback, useState } from 'react'

// Import types from shared types file to avoid circular dependencies
import type { Product } from './types'
// Re-export for backward compatibility
export type { Product } from './types'

import { demoProducts } from './demo/list'


/**
 * ═══════════════════════════════════════════════════════════════════════════
 * ProductListProps
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * Props for configuring a versatile product list component with list, grid,
 * carousel, and picker variants.
 */
export interface ProductListProps {
  data?: {
    /** Array of products to display in the list. */
    products?: Product[]
  }
  actions?: {
    /** Called when a user selects a product from the list. */
    onSelectProduct?: (product: Product) => void
    /** Called when products are added to cart (picker variant only). */
    onAddToCart?: (products: Product[]) => void
  }
  appearance?: {
    /**
     * Layout variant for displaying products.
     * @default "list"
     */
    variant?: 'list' | 'grid' | 'carousel' | 'picker'
    /**
     * Currency code for price formatting (e.g., "USD", "EUR").
     * @default "EUR"
     */
    currency?: string
    /**
     * Number of columns for grid variant.
     * @default 4
     */
    columns?: 3 | 4
    /**
     * Custom label for the add to cart button (picker variant).
     * @default "Add to cart"
     */
    buttonLabel?: string
  }
  control?: {
    /** Index of the currently selected product. */
    selectedProductIndex?: number
  }
}

// Horizontal card for list variant
function ProductHorizontalCard({
  product,
  selected,
  onSelect,
  formatCurrency
}: {
  product: Product
  selected: boolean
  onSelect: () => void
  formatCurrency: (value: number) => string
}) {
  return (
    <button
      onClick={onSelect}
      disabled={!product.inStock}
      className={cn(
        'w-full flex items-center gap-3 rounded-[12px] border p-2 text-left transition-all cursor-pointer',
        selected
          ? 'bg-card border-foreground ring-1 ring-foreground'
          : 'bg-card border-border hover:border-foreground/50',
        !product.inStock && 'opacity-50 !cursor-not-allowed'
      )}
    >
      <div className="relative h-16 w-16 flex-shrink-0 rounded-md overflow-hidden">
        {product.image ? (
          <img
            src={product.image}
            alt={product.name}
            className="h-full w-full object-contain bg-muted/30"
          />
        ) : (
          <div className="h-full w-full bg-muted" />
        )}
        {product.badge && (
          <span
            className={cn(
              'absolute top-1 left-1 px-1 py-0.5 text-[8px] font-medium rounded',
              product.badge.startsWith('-')
                ? 'bg-foreground text-background'
                : 'bg-background text-foreground border border-border'
            )}
          >
            {product.badge}
          </span>
        )}
      </div>
      <div className="flex-1 min-w-0 space-y-0.5">
        {product.name && <p className="text-sm font-medium truncate">{product.name}</p>}
        {product.description && (
          <p className="text-xs truncate text-muted-foreground">
            {product.description}
          </p>
        )}
        <div className="flex items-center gap-2">
          {product.price !== undefined && (
            <span className="text-sm font-semibold">
              {formatCurrency(product.price)}
            </span>
          )}
          {product.originalPrice && (
            <span className="text-xs line-through text-muted-foreground">
              {formatCurrency(product.originalPrice)}
            </span>
          )}
        </div>
      </div>
      <ChevronRight className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
    </button>
  )
}

// List variant
function ListVariant({
  products,
  selected,
  onSelect,
  formatCurrency
}: {
  products: Product[]
  selected: number | undefined
  onSelect: (product: Product, index: number) => void
  formatCurrency: (value: number) => string
}) {
  return (
    <div className="w-full space-y-2 p-1 sm:p-0">
      {products.slice(0, 4).map((product, index) => (
        <ProductHorizontalCard
          key={index}
          product={product}
          selected={selected === index}
          onSelect={() => onSelect(product, index)}
          formatCurrency={formatCurrency}
        />
      ))}
    </div>
  )
}

// Grid variant
function GridVariant({
  products,
  selected,
  onSelect,
  formatCurrency,
  columns
}: {
  products: Product[]
  selected: number | undefined
  onSelect: (product: Product, index: number) => void
  formatCurrency: (value: number) => string
  columns: 3 | 4
}) {
  const displayProducts = products.slice(0, columns)

  return (
    <div className="w-full p-1 sm:p-0">
      <div
        className={cn(
          'grid gap-2 sm:gap-3 grid-cols-2',
          columns === 4 ? 'sm:grid-cols-4' : 'sm:grid-cols-3'
        )}
      >
        {displayProducts.map((product, index) => (
          <button
            key={index}
            onClick={() => onSelect(product, index)}
            disabled={!product.inStock}
            className={cn(
              'rounded-[12px] border text-left transition-all overflow-hidden cursor-pointer',
              selected === index
                ? 'bg-card border-foreground ring-1 ring-foreground'
                : 'bg-card border-border hover:border-foreground/50',
              !product.inStock && 'opacity-50 !cursor-not-allowed'
            )}
          >
            <div className="relative">
              {product.image ? (
                <img
                  src={product.image}
                  alt={product.name}
                  className="aspect-square lg:h-28 lg:aspect-auto w-full object-contain bg-muted/30"
                />
              ) : (
                <div className="aspect-square lg:h-28 lg:aspect-auto w-full bg-muted" />
              )}
              {product.badge && (
                <span
                  className={cn(
                    'absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-medium rounded',
                    product.badge.startsWith('-')
                      ? 'bg-foreground text-background'
                      : 'bg-background text-foreground border border-border'
                  )}
                >
                  {product.badge}
                </span>
              )}
            </div>
            <div className="p-2 sm:p-3 space-y-0.5 sm:space-y-1">
              {product.name && (
                <p className="text-xs sm:text-sm font-medium line-clamp-1">
                  {product.name}
                </p>
              )}
              {product.description && (
                <p className="text-[10px] sm:text-xs line-clamp-1 text-muted-foreground">
                  {product.description}
                </p>
              )}
              <div className="flex items-center justify-between">
                <div className="flex items-baseline gap-1">
                  {product.price !== undefined && (
                    <span className="text-xs sm:text-sm font-semibold">
                      {formatCurrency(product.price)}
                    </span>
                  )}
                  {product.originalPrice && (
                    <span className="text-[10px] sm:text-xs line-through text-muted-foreground">
                      {formatCurrency(product.originalPrice)}
                    </span>
                  )}
                </div>
                {product.rating && (
                  <div className="hidden sm:flex items-center gap-0.5 text-xs text-muted-foreground">
                    <Star className="h-3 w-3 fill-yellow-400 text-yellow-400" />
                    {product.rating}
                  </div>
                )}
              </div>
              {!product.inStock && (
                <p className="text-[10px] sm:text-xs text-destructive">
                  Out of stock
                </p>
              )}
            </div>
          </button>
        ))}
      </div>
    </div>
  )
}

// Carousel variant
function CarouselVariant({
  products,
  selected,
  onSelect,
  formatCurrency
}: {
  products: Product[]
  selected: number | undefined
  onSelect: (product: Product, index: number) => void
  formatCurrency: (value: number) => string
}) {
  const [currentIndex, setCurrentIndex] = useState(0)

  const CARD_WIDTH = 160
  const GAP = 12
  const desktopTransform = currentIndex * (CARD_WIDTH + GAP)
  const tabletMaxIndex = Math.max(0, products.length - 2)

  const goLeft = () => {
    if (currentIndex > 0) {
      setCurrentIndex(currentIndex - 1)
    }
  }

  // Horizontal card for mobile/tablet
  const HorizontalCard = ({ product, index }: { product: Product; index: number }) => (
    <button
      type="button"
      onClick={() => onSelect(product, index)}
      disabled={!product.inStock}
      className={cn(
        'w-full rounded-[12px] border text-left cursor-pointer',
        'flex items-center gap-3 p-2',
        selected === index
          ? 'bg-card border-foreground shadow-[0_0_0_1px] shadow-foreground'
          : 'bg-card border-border hover:border-foreground/50',
        !product.inStock && 'opacity-50 !cursor-not-allowed'
      )}
    >
      <div className="relative h-16 w-16 flex-shrink-0 rounded overflow-hidden bg-muted/30">
        {product.image && (
          <img
            src={product.image}
            alt={product.name}
            className="h-full w-full object-contain"
          />
        )}
        {product.badge && (
          <span
            className={cn(
              'absolute top-1 left-1 px-1 py-0.5 text-[8px] font-medium rounded',
              product.badge.startsWith('-')
                ? 'bg-foreground text-background'
                : 'bg-background text-foreground border'
            )}
          >
            {product.badge}
          </span>
        )}
      </div>
      <div className="flex-1 min-w-0">
        {product.name && <p className="text-sm font-medium truncate">{product.name}</p>}
        {product.description && (
          <p className="text-xs text-muted-foreground truncate">
            {product.description}
          </p>
        )}
        {product.price !== undefined && <p className="text-sm font-semibold">{formatCurrency(product.price)}</p>}
      </div>
    </button>
  )

  // Dots component
  const Dots = ({
    count,
    active,
    onDotClick
  }: {
    count: number
    active: number
    onDotClick: (i: number) => void
  }) => (
    <div className="flex justify-center gap-1.5 mt-3">
      {Array.from({ length: count }).map((_, i) => (
        <button
          key={i}
          type="button"
          onClick={() => onDotClick(i)}
          aria-label={`Go to slide ${i + 1}`}
          className={cn(
            'h-1.5 rounded-full transition-all duration-300 cursor-pointer',
            i === active
              ? 'w-4 bg-foreground'
              : 'w-1.5 bg-foreground/30 hover:bg-foreground/50'
          )}
        />
      ))}
    </div>
  )

  const mobileProduct = products[currentIndex]
  const tabletProducts = [
    products[Math.min(currentIndex, tabletMaxIndex)],
    products[Math.min(currentIndex, tabletMaxIndex) + 1]
  ].filter(Boolean)

  return (
    <div className="w-full">
      {/* Mobile: 1 card + dots */}
      <div className="sm:hidden px-0.5">
        <div
          key={currentIndex}
          className="w-full animate-in fade-in slide-in-from-right-4 duration-300"
        >
          {mobileProduct && <HorizontalCard product={mobileProduct} index={currentIndex} />}
        </div>
        <Dots
          count={products.length}
          active={currentIndex}
          onDotClick={(i) => setCurrentIndex(i)}
        />
      </div>

      {/* Tablet: 2 cards + dots */}
      <div className="hidden sm:block lg:hidden px-0.5">
        <div
          key={Math.min(currentIndex, tabletMaxIndex)}
          className="grid grid-cols-2 gap-2 animate-in fade-in slide-in-from-right-4 duration-300"
        >
          {tabletProducts.map((product, i) => {
            const productIndex = Math.min(currentIndex, tabletMaxIndex) + i
            return <HorizontalCard key={productIndex} product={product} index={productIndex} />
          })}
        </div>
        <Dots
          count={tabletMaxIndex + 1}
          active={Math.min(currentIndex, tabletMaxIndex)}
          onDotClick={(i) => setCurrentIndex(i)}
        />
      </div>

      {/* Desktop: multi-card carousel */}
      {(() => {
        const desktopMaxIndex = Math.max(0, products.length - 4)
        const isAtEnd = currentIndex >= desktopMaxIndex
        return (
          <div className="hidden lg:block relative">
            <button
              type="button"
              onClick={goLeft}
              disabled={currentIndex === 0}
              aria-label="Previous product"
              className={cn(
                'absolute left-2 top-1/2 -translate-y-1/2 z-10 h-8 w-8 rounded-full bg-background/80 backdrop-blur-sm border shadow-sm flex items-center justify-center cursor-pointer',
                currentIndex === 0 ? 'opacity-0' : 'hover:bg-background'
              )}
            >
              <ChevronLeft className="h-4 w-4" />
            </button>

            <button
              type="button"
              onClick={() => {
                if (currentIndex < desktopMaxIndex) {
                  setCurrentIndex(currentIndex + 1)
                }
              }}
              disabled={isAtEnd}
              aria-label="Next product"
              className={cn(
                'absolute right-2 top-1/2 -translate-y-1/2 z-10 h-8 w-8 rounded-full bg-background/80 backdrop-blur-sm border shadow-sm flex items-center justify-center cursor-pointer',
                isAtEnd ? 'opacity-0' : 'hover:bg-background'
              )}
            >
              <ChevronRight className="h-4 w-4" />
            </button>

            <div className="overflow-hidden py-1 -mx-1">
              <div
                className="flex gap-3 transition-transform duration-300 ease-out px-1"
                style={{ transform: `translateX(-${desktopTransform}px)` }}
              >
                {products.map((product, index) => (
                  <button
                    type="button"
                    key={index}
                    onClick={() => onSelect(product, index)}
                    disabled={!product.inStock}
                    className={cn(
                      'flex-shrink-0 w-40 rounded-[12px] border text-left cursor-pointer',
                      selected === index
                        ? 'bg-card border-foreground ring-1 ring-foreground'
                        : 'bg-card border-border hover:border-foreground/50',
                      !product.inStock && 'opacity-50 !cursor-not-allowed'
                    )}
                  >
                    <div className="relative h-28 w-full bg-muted/30 rounded-t-[11px] overflow-hidden">
                      {product.image && (
                        <img
                          src={product.image}
                          alt={product.name}
                          className="h-full w-full object-contain"
                        />
                      )}
                      {product.badge && (
                        <span
                          className={cn(
                            'absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-medium rounded',
                            product.badge.startsWith('-')
                              ? 'bg-foreground text-background'
                              : 'bg-background text-foreground border'
                          )}
                        >
                          {product.badge}
                        </span>
                      )}
                    </div>
                    <div className="p-3 space-y-1">
                      {product.name && (
                        <p className="text-sm font-medium truncate">
                          {product.name}
                        </p>
                      )}
                      {product.description && (
                        <p className="text-xs text-muted-foreground truncate">
                          {product.description}
                        </p>
                      )}
                      {product.price !== undefined && (
                        <p className="text-sm font-semibold">
                          {formatCurrency(product.price)}
                        </p>
                      )}
                    </div>
                  </button>
                ))}
              </div>
            </div>
          </div>
        )
      })()}
    </div>
  )
}

// Picker variant (multi-select with add to cart)
function PickerVariant({
  products,
  formatCurrency,
  onAddToCart,
  buttonLabel = 'Add to cart'
}: {
  products: Product[]
  formatCurrency: (value: number) => string
  onAddToCart?: (products: Product[]) => void
  buttonLabel?: string
}) {
  const [selectedIndexes, setSelectedIndexes] = useState<Set<number>>(new Set())

  const handleSelect = useCallback((index: number, product: Product) => {
    if (!product.inStock) return

    setSelectedIndexes((prev) => {
      const newSet = new Set(prev)
      if (newSet.has(index)) {
        newSet.delete(index)
      } else {
        newSet.add(index)
      }
      return newSet
    })
  }, [])

  const handleSelectAll = useCallback(() => {
    const availableIndexes = products
      .map((p, i) => (p.inStock ? i : -1))
      .filter((i) => i !== -1)
    const allSelected = availableIndexes.every((i) => selectedIndexes.has(i))

    if (allSelected) {
      setSelectedIndexes(new Set())
    } else {
      setSelectedIndexes(new Set(availableIndexes))
    }
  }, [products, selectedIndexes])

  const handleAddToCart = useCallback(() => {
    const selectedProducts = products.filter((_, i) => selectedIndexes.has(i))
    onAddToCart?.(selectedProducts)
  }, [products, selectedIndexes, onAddToCart])

  const availableIndexes = products
    .map((p, i) => (p.inStock ? i : -1))
    .filter((i) => i !== -1)
  const allSelected =
    availableIndexes.length > 0 &&
    availableIndexes.every((i) => selectedIndexes.has(i))

  const totalPrice = products
    .filter((_, i) => selectedIndexes.has(i))
    .reduce((sum, p) => sum + (p.price ?? 0), 0)

  return (
    <div className="w-full space-y-3 rounded-md sm:rounded-lg p-4 sm:p-0">
      {/* Mobile: Card view */}
      <div className="sm:hidden space-y-2 px-0.5">
        {products.map((product, index) => (
          <button
            key={index}
            type="button"
            onClick={() => handleSelect(index, product)}
            disabled={!product.inStock}
            className={cn(
              'w-full flex items-center gap-3 rounded-md sm:rounded-lg border bg-card p-2 text-left transition-all cursor-pointer',
              selectedIndexes.has(index)
                ? 'border-foreground ring-1 ring-foreground'
                : 'border-border hover:border-foreground/30',
              !product.inStock && 'opacity-50 !cursor-not-allowed'
            )}
          >
            {/* Checkbox */}
            <div
              className={cn(
                'flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border transition-colors',
                selectedIndexes.has(index)
                  ? 'bg-foreground border-foreground text-background'
                  : 'border-border'
              )}
            >
              {selectedIndexes.has(index) && <Check className="h-3 w-3" />}
            </div>

            {/* Image */}
            <div className="h-12 w-12 flex-shrink-0 rounded overflow-hidden bg-muted/30">
              {product.image && (
                <img
                  src={product.image}
                  alt={product.name}
                  className="h-full w-full object-contain"
                />
              )}
            </div>

            {/* Content */}
            <div className="flex-1 min-w-0">
              {product.name && <p className="text-sm font-medium truncate">{product.name}</p>}
              {product.description && (
                <p className="text-xs text-muted-foreground truncate">
                  {product.description}
                </p>
              )}
            </div>

            {/* Price */}
            <div className="text-right flex-shrink-0">
              {product.price !== undefined && (
                <p className="text-sm font-semibold">
                  {formatCurrency(product.price)}
                </p>
              )}
              {product.originalPrice && (
                <p className="text-xs text-muted-foreground line-through">
                  {formatCurrency(product.originalPrice)}
                </p>
              )}
            </div>
          </button>
        ))}
      </div>

      {/* Desktop: Table view */}
      <div className="hidden sm:block overflow-x-auto rounded-md sm:rounded-lg mb-0">
        <table className="w-full text-sm">
          <thead className="border-b bg-muted/50">
            <tr>
              <th className="w-10 px-3 py-3">
                <button
                  type="button"
                  onClick={handleSelectAll}
                  className={cn(
                    'flex h-4 w-4 items-center justify-center rounded border transition-colors',
                    allSelected
                      ? 'bg-foreground border-foreground text-background'
                      : 'border-border hover:border-foreground/50'
                  )}
                  aria-label="Select all products"
                >
                  {allSelected && <Check className="h-3 w-3" />}
                </button>
              </th>
              <th className="px-3 py-3 text-left font-medium text-muted-foreground">
                Product
              </th>
              <th className="px-3 py-3 text-right font-medium text-muted-foreground">
                Price
              </th>
            </tr>
          </thead>
          <tbody>
            {products.map((product, index) => (
              <tr
                key={index}
                onClick={() => handleSelect(index, product)}
                className={cn(
                  'border-b border-border last:border-0 transition-colors',
                  product.inStock
                    ? 'cursor-pointer hover:bg-muted/30'
                    : 'opacity-50 cursor-not-allowed'
                )}
              >
                <td className="px-3 py-3">
                  <div
                    className={cn(
                      'flex h-4 w-4 items-center justify-center rounded border transition-colors',
                      selectedIndexes.has(index)
                        ? 'bg-foreground border-foreground text-background'
                        : 'border-border'
                    )}
                  >
                    {selectedIndexes.has(index) && (
                      <Check className="h-3 w-3" />
                    )}
                  </div>
                </td>
                <td className="px-3 py-3">
                  <div className="flex items-center gap-3">
                    <div className="h-10 w-10 flex-shrink-0 rounded overflow-hidden bg-muted/30">
                      {product.image && (
                        <img
                          src={product.image}
                          alt={product.name}
                          className="h-full w-full object-contain"
                        />
                      )}
                    </div>
                    <div className="min-w-0">
                      {product.name && <p className="font-medium truncate">{product.name}</p>}
                      {product.description && (
                        <p className="text-xs text-muted-foreground truncate">
                          {product.description}
                        </p>
                      )}
                      {!product.inStock && (
                        <p className="text-xs text-destructive">Out of stock</p>
                      )}
                    </div>
                  </div>
                </td>
                <td className="px-3 py-3 text-right">
                  {product.price !== undefined && (
                    <p className="font-semibold">
                      {formatCurrency(product.price)}
                    </p>
                  )}
                  {product.originalPrice && (
                    <p className="text-xs text-muted-foreground line-through">
                      {formatCurrency(product.originalPrice)}
                    </p>
                  )}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Add to cart button */}
      <div className="flex items-center justify-between gap-4 p-3 border-t-1">
        <div className="text-xs sm:text-sm text-muted-foreground">
          {selectedIndexes.size > 0 ? (
            <span>
              {selectedIndexes.size} item{selectedIndexes.size !== 1 ? 's' : ''}{' '}
              selected
              {' · '}
              <span className="font-medium text-foreground">
                {formatCurrency(totalPrice)}
              </span>
            </span>
          ) : (
            <span>Select products to add to cart</span>
          )}
        </div>
        <Button
          onClick={handleAddToCart}
          disabled={selectedIndexes.size === 0}
          size="sm"
        >
          <ShoppingCart className="h-4 w-4 mr-1.5" />
          {buttonLabel}
        </Button>
      </div>
    </div>
  )
}

/**
 * A versatile product list component with list, grid, carousel, and picker variants.
 * Supports product selection, badges, ratings, and add to cart functionality.
 *
 * Features:
 * - Multiple layout variants (list, grid, carousel, picker)
 * - Product cards with images, badges, and pricing
 * - Original price strikethrough for discounts
 * - Star ratings display
 * - Out of stock indication
 * - Single product selection (list/grid/carousel)
 * - Multi-select with cart total (picker)
 * - Responsive layouts for all screen sizes
 * - Animated carousel navigation
 *
 * @component
 * @example
 * ```tsx
 * <ProductList
 *   data={{
 *     products: [
 *       { id: "1", name: "Sneakers", price: 99, image: "/shoe.jpg", rating: 4.5 },
 *       { id: "2", name: "Boots", price: 149, originalPrice: 199, badge: "-25%" }
 *     ]
 *   }}
 *   actions={{
 *     onSelectProduct: (product) => console.log("Selected:", product),
 *     onAddToCart: (products) => console.log("Cart:", products)
 *   }}
 *   appearance={{
 *     variant: "grid",
 *     currency: "USD",
 *     columns: 4
 *   }}
 * />
 * ```
 */
export function ProductList({ data, actions, appearance, control }: ProductListProps) {
  const resolved: NonNullable<ProductListProps['data']> = data ?? { products: demoProducts }
  const products = resolved.products ?? []
  const onSelectProduct = actions?.onSelectProduct
  const onAddToCart = actions?.onAddToCart
  const variant = appearance?.variant ?? 'list'
  const currency = appearance?.currency ?? 'EUR'
  const columns = appearance?.columns ?? 4
  const buttonLabel = appearance?.buttonLabel
  const selectedProductIndex = control?.selectedProductIndex
  const [selected, setSelected] = useState<number | undefined>(selectedProductIndex)

  const formatCurrency = (value: number) => {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency,
      minimumFractionDigits: 0
    }).format(value)
  }

  const handleSelect = (product: Product, index: number) => {
    setSelected(index)
    onSelectProduct?.(product)
  }

  if (variant === 'grid') {
    return (
      <GridVariant
        products={products}
        selected={selected}
        onSelect={handleSelect}
        formatCurrency={formatCurrency}
        columns={columns}
      />
    )
  }

  if (variant === 'carousel') {
    return (
      <CarouselVariant
        products={products}
        selected={selected}
        onSelect={handleSelect}
        formatCurrency={formatCurrency}
      />
    )
  }

  if (variant === 'picker') {
    return (
      <PickerVariant
        products={products}
        formatCurrency={formatCurrency}
        onAddToCart={onAddToCart}
        buttonLabel={buttonLabel}
      />
    )
  }

  return (
    <ListVariant
      products={products}
      selected={selected}
      onSelect={handleSelect}
      formatCurrency={formatCurrency}
    />
  )
}
````

### `registry/list/demo/list.ts`

```ts
// Demo data for List category components
// This file contains sample data used for component previews and documentation

import type { Product } from '../types'

// Default products for ProductList
export const demoProducts: Product[] = [
  {
    name: "Air Force 1 '07",
    description: 'Nike',
    price: 119,
    image: 'https://ui.manifest.build/demo/shoe-1.png',
    rating: 4.9,
    badge: 'New',
    inStock: true
  },
  {
    name: 'Air Max 90',
    description: 'Nike',
    price: 140,
    image: 'https://ui.manifest.build/demo/shoe-2.png',
    rating: 4.8,
    inStock: true
  },
  {
    name: 'Air Max Plus',
    description: 'Nike',
    price: 170,
    originalPrice: 190,
    image: 'https://ui.manifest.build/demo/shoe-4.png',
    rating: 4.7,
    badge: '-10%',
    inStock: true
  },
  {
    name: 'Dunk Low',
    description: 'Nike',
    price: 115,
    image: 'https://ui.manifest.build/demo/shoe-3.png',
    rating: 4.6,
    inStock: true
  },
  {
    name: 'Jordan 1 Low',
    description: 'Nike',
    price: 135,
    image: 'https://ui.manifest.build/demo/shoe-1.png',
    rating: 4.8,
    inStock: true
  },
  {
    name: 'Blazer Mid',
    description: 'Nike',
    price: 105,
    image: 'https://ui.manifest.build/demo/shoe-2.png',
    rating: 4.5,
    inStock: true
  },
]

// Table columns
export const demoTableColumns = [
  { header: 'Name', accessor: 'name' },
  { header: 'Email', accessor: 'email' },
  { header: 'Status', accessor: 'status' },
]

// Table rows
export const demoTableRows = [
  { name: 'John Doe', email: 'john@example.com', status: 'Active' },
  { name: 'Jane Smith', email: 'jane@example.com', status: 'Pending' },
  { name: 'Bob Johnson', email: 'bob@example.com', status: 'Active' },
]

// Table variant: API Usage (default)
export const demoApiUsageColumns = [
  { header: 'Model', accessor: 'model', sortable: true },
  { header: 'Total Tokens', accessor: 'totalTokens', sortable: true, align: 'right' as const },
]

export const demoApiUsageRows = [
  { model: 'gpt-5', totalTokens: 2267482 },
  { model: 'claude-3.5-sonnet', totalTokens: 647528 },
  { model: 'gemini-pro', totalTokens: 428190 },
  { model: 'llama-3', totalTokens: 312475 },
]

// Table variant: Models (single select)
export const demoModelsColumns = [
  { header: 'Model', accessor: 'model', sortable: true },
  { header: 'Provider', accessor: 'provider', sortable: true },
  { header: 'Context Window', accessor: 'contextWindow', sortable: true, align: 'right' as const },
]

export const demoModelsRows = [
  { model: 'GPT-5', provider: 'OpenAI', contextWindow: '128K' },
  { model: 'Claude 3.5 Sonnet', provider: 'Anthropic', contextWindow: '200K' },
  { model: 'Gemini Pro', provider: 'Google', contextWindow: '1M' },
  { model: 'Llama 3', provider: 'Meta', contextWindow: '128K' },
]

// Table variant: Export Data (multi select)
export const demoExportColumns = [
  { header: 'Date', accessor: 'date', sortable: true },
  { header: 'Event', accessor: 'event', sortable: true },
  { header: 'Users', accessor: 'users', sortable: true, align: 'right' as const },
]

export const demoExportRows = [
  { date: '2025-01-15', event: 'Page View', users: 1243 },
  { date: '2025-01-15', event: 'Sign Up', users: 87 },
  { date: '2025-01-14', event: 'Page View', users: 1105 },
  { date: '2025-01-14', event: 'Purchase', users: 42 },
]
```
