{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "product-detail-page",
  "title": "Product Detail Page",
  "type": "registry:page",
  "description": "Flagship e-commerce product detail page with multi-angle gallery, finish & cable selectors, stock urgency counter, warranty upsell, flash sale countdown bar, frequently bought together bundle, technical specs sheet, customer reviews breakdown, and community Q&A.",
  "categories": [
    "commerce",
    "ecommerce"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/product-detail-page/product-detail-page.ts",
      "content": "import { LitElement, css, html } from 'lit'\nimport {\n  ChevronRight,\n  Flame,\n  Heart,\n  Minus,\n  Plus,\n  RotateCcw,\n  Share2,\n  ShieldCheck,\n  ShoppingCart,\n  Star,\n  Truck,\n  Zap,\n} from 'lucide'\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/button/button'\nimport '../../components/input/input'\nimport type { UipInput } from '../../components/input/input'\nimport '../../components/separator/separator'\n\nexport interface ProductVariant {\n  id: string\n  name: string\n  colorHex: string\n  sku: string\n  inStock: boolean\n  stockCount: number\n  price: number\n  originalPrice: number\n  images: { id: string; url: string; alt: string }[]\n}\n\nexport const productVariants: ProductVariant[] = [\n  {\n    id: 'var-black',\n    name: 'Obsidian Matte Black',\n    colorHex: '#18181b',\n    sku: 'APX-950-BLK',\n    inStock: true,\n    stockCount: 14,\n    price: 599,\n    originalPrice: 749,\n    images: [\n      {\n        id: 'img-1',\n        url: 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=900&auto=format&fit=crop&q=80',\n        alt: 'Apex Pro Reference Studio Headphones on Wood Desk',\n      },\n      {\n        id: 'img-2',\n        url: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=900&auto=format&fit=crop&q=80',\n        alt: 'Apex Pro CNC Aluminum Driver Open Back View',\n      },\n      {\n        id: 'img-3',\n        url: 'https://images.unsplash.com/photo-1583394838336-acd977736f90?w=900&auto=format&fit=crop&q=80',\n        alt: 'Apex Pro Lambskin Magnetic Earpads Angle',\n      },\n      {\n        id: 'img-4',\n        url: 'https://images.unsplash.com/photo-1484704849700-f032a568e944?w=900&auto=format&fit=crop&q=80',\n        alt: 'Apex Pro Studio Audio Cable Interconnects',\n      },\n    ],\n  },\n  {\n    id: 'var-silver',\n    name: 'Lunar Silver Aluminum',\n    colorHex: '#94a3b8',\n    sku: 'APX-950-SLV',\n    inStock: true,\n    stockCount: 6,\n    price: 599,\n    originalPrice: 749,\n    images: [\n      {\n        id: 'img-2',\n        url: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=900&auto=format&fit=crop&q=80',\n        alt: 'Apex Pro Lunar Silver Open Back View',\n      },\n      {\n        id: 'img-1',\n        url: 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=900&auto=format&fit=crop&q=80',\n        alt: 'Apex Pro Silver Edition Desk Shot',\n      },\n      {\n        id: 'img-3',\n        url: 'https://images.unsplash.com/photo-1583394838336-acd977736f90?w=900&auto=format&fit=crop&q=80',\n        alt: 'Apex Pro Earpads Detail',\n      },\n    ],\n  },\n  {\n    id: 'var-navy',\n    name: 'Midnight Studio Navy',\n    colorHex: '#1e293b',\n    sku: 'APX-950-NVY',\n    inStock: true,\n    stockCount: 3,\n    price: 629,\n    originalPrice: 779,\n    images: [\n      {\n        id: 'img-3',\n        url: 'https://images.unsplash.com/photo-1583394838336-acd977736f90?w=900&auto=format&fit=crop&q=80',\n        alt: 'Apex Pro Midnight Navy Studio Edition',\n      },\n      {\n        id: 'img-1',\n        url: 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=900&auto=format&fit=crop&q=80',\n        alt: 'Apex Pro Navy Perspective',\n      },\n      {\n        id: 'img-2',\n        url: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=900&auto=format&fit=crop&q=80',\n        alt: 'Apex Pro Navy Driver Housing',\n      },\n    ],\n  },\n]\n\n/**\n * <uip-product-detail-page> — the registry ProductDetailPage block as a web component.\n *\n * Class strings, variants, and gallery/buy-box behavior are React's verbatim.\n * React state becomes { state: true } fields; `selectedVariantId` is an\n * attribute. React's `alert()` calls become `add-to-cart` (variant, cable,\n * quantity, warranty, total) / `express-buy` (total) / `share` CustomEvents\n * (bubbles, composed; payloads in `detail`).\n *\n * One deliberate departure: React composes five sibling blocks below the\n * buy box (flash-sale-offer-bar, frequently-bought-together,\n * technical-specs-sheet, customer-reviews, product-qa-community), but blocks\n * never import sibling blocks (one block = one install unit). This element\n * renders the breadcrumb + gallery/buy-box hero split; compose the sibling\n * <uip-*> blocks after it on the page, as the demo does.\n */\nexport class UipProductDetailPage 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    selectedVariantId: { attribute: 'selected-variant-id' },\n    activeImageIndex: { state: true },\n    quantity: { state: true },\n    selectedCable: { state: true },\n    isWishlisted: { state: true },\n    addExtendedWarranty: { state: true },\n    zipCode: { state: true },\n  }\n\n  selectedVariantId = 'var-black'\n  private activeImageIndex = 0\n  private quantity = 1\n  private selectedCable: '4.4mm' | '3.5mm' | 'xlr' = '4.4mm'\n  private isWishlisted = false\n  private addExtendedWarranty = true\n  private zipCode = '94103'\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-detail-page')\n  }\n\n  private get currentVariant(): ProductVariant {\n    return productVariants.find((v) => v.id === this.selectedVariantId) || productVariants[0]!\n  }\n\n  private get activeImageUrl() {\n    const v = this.currentVariant\n    return v.images[this.activeImageIndex]?.url || v.images[0]!.url\n  }\n\n  private get finalPrice() {\n    let total = this.currentVariant.price * this.quantity\n    if (this.addExtendedWarranty) total += 49\n    return total\n  }\n\n  private selectVariant(v: ProductVariant) {\n    const changed = v.id !== this.selectedVariantId\n    this.selectedVariantId = v.id\n    this.activeImageIndex = 0\n    if (changed) {\n      this.dispatchEvent(\n        new CustomEvent('variant-change', { detail: { variantId: v.id }, bubbles: true, composed: true }),\n      )\n    }\n  }\n\n  private incrementQty() {\n    if (this.quantity < this.currentVariant.stockCount) this.quantity += 1\n  }\n\n  private decrementQty() {\n    if (this.quantity > 1) this.quantity -= 1\n  }\n\n  private handleAddToCart() {\n    this.dispatchEvent(\n      new CustomEvent('add-to-cart', {\n        detail: {\n          variantId: this.currentVariant.id,\n          cable: this.selectedCable,\n          quantity: this.quantity,\n          warranty: this.addExtendedWarranty,\n          total: this.finalPrice,\n        },\n        bubbles: true,\n        composed: true,\n      }),\n    )\n  }\n\n  private handleExpressBuy() {\n    this.dispatchEvent(\n      new CustomEvent('express-buy', { detail: { total: this.finalPrice }, bubbles: true, composed: true }),\n    )\n  }\n\n  render() {\n    const v = this.currentVariant\n    return html`\n      <div data-slot=\"product-detail-page\" class=\"bg-background text-foreground w-full space-y-10\">\n        <nav class=\"text-muted-foreground flex items-center gap-1.5 text-xs\">\n          <span class=\"hover:text-foreground cursor-pointer\">Storefront</span>\n          ${icon(ChevronRight, 'chevron-right', 'size-3.5')}\n          <span class=\"hover:text-foreground cursor-pointer\">Pro Audio & Reference</span>\n          ${icon(ChevronRight, 'chevron-right', 'size-3.5')}\n          <span class=\"hover:text-foreground cursor-pointer\">Planar Magnetic</span>\n          ${icon(ChevronRight, 'chevron-right', 'size-3.5')}\n          <span class=\"text-foreground font-medium\">APX-950-PRO Reference Monitor</span>\n        </nav>\n\n        <div class=\"grid grid-cols-1 gap-8 lg:grid-cols-12 lg:gap-12\">\n          <div class=\"space-y-4 lg:col-span-7\">\n            <div class=\"border-border bg-muted/30 relative aspect-4/3 w-full overflow-hidden rounded-xl border shadow-xs\">\n              <img\n                src=${this.activeImageUrl}\n                alt=${v.name}\n                class=\"size-full object-cover transition-transform duration-300 hover:scale-105\"\n              />\n              <div class=\"absolute top-3 left-3 flex flex-wrap gap-1.5\">\n                <uip-badge variant=\"default\" class=\"h-6 font-mono text-xs font-semibold uppercase\">2026 Flagship</uip-badge>\n                <uip-badge variant=\"secondary\" class=\"text-success h-6 font-mono text-xs\">Laboratory Calibrated</uip-badge>\n              </div>\n              <div class=\"absolute top-3 right-3 flex flex-col gap-2\">\n                <button\n                  type=\"button\"\n                  aria-label=\"Toggle wishlist\"\n                  class=${`border-border bg-background/90 flex size-8 items-center justify-center rounded-full border shadow-2xs backdrop-blur-xs transition-colors ${\n                    this.isWishlisted ? 'text-destructive' : 'text-muted-foreground hover:text-foreground'\n                  }`}\n                  @click=${() => {\n                    this.isWishlisted = !this.isWishlisted\n                  }}\n                >\n                  ${icon(Heart, 'heart', `size-4 ${this.isWishlisted ? 'fill-current' : ''}`)}\n                </button>\n                <button\n                  type=\"button\"\n                  aria-label=\"Share product\"\n                  class=\"border-border bg-background/90 text-muted-foreground hover:text-foreground flex size-8 items-center justify-center rounded-full border shadow-2xs backdrop-blur-xs transition-colors\"\n                  @click=${() => this.dispatchEvent(new CustomEvent('share', { bubbles: true, composed: true }))}\n                >\n                  ${icon(Share2, 'share-2', 'size-4')}\n                </button>\n              </div>\n            </div>\n\n            <div class=\"grid grid-cols-4 gap-3\">\n              ${v.images.map(\n                (img, idx) => html`<button\n                  type=\"button\"\n                  class=${`bg-muted/20 relative aspect-4/3 overflow-hidden rounded-lg border transition-colors ${\n                    this.activeImageIndex === idx\n                      ? 'border-primary ring-primary/40 ring-2'\n                      : 'border-border opacity-70 hover:opacity-100'\n                  }`}\n                  @click=${() => {\n                    this.activeImageIndex = idx\n                  }}\n                >\n                  <img src=${img.url} alt=${img.alt} class=\"size-full object-cover\" />\n                </button>`,\n              )}\n            </div>\n\n            <div class=\"border-border bg-muted/20 grid grid-cols-3 gap-3 rounded-lg border p-3 text-center text-xs\">\n              <div class=\"flex flex-col items-center gap-1\">\n                ${icon(Truck, 'truck', 'text-primary size-4')}\n                <span class=\"text-foreground font-medium\">Free 2-Day Air</span>\n                <span class=\"text-muted-foreground text-xs\">Orders over $150</span>\n              </div>\n              <div class=\"border-border/80 flex flex-col items-center gap-1 border-x px-2\">\n                ${icon(RotateCcw, 'rotate-ccw', 'text-success size-4')}\n                <span class=\"text-foreground font-medium\">45-Day Trial</span>\n                <span class=\"text-muted-foreground text-xs\">Zero-risk return</span>\n              </div>\n              <div class=\"flex flex-col items-center gap-1\">\n                ${icon(ShieldCheck, 'shield-check', 'text-info size-4')}\n                <span class=\"text-foreground font-medium\">3-Yr Warranty</span>\n                <span class=\"text-muted-foreground text-xs\">Direct factory support</span>\n              </div>\n            </div>\n          </div>\n\n          <div class=\"space-y-6 lg:col-span-5\">\n            <div class=\"space-y-2\">\n              <div class=\"text-muted-foreground flex items-center gap-2 font-mono text-xs\">\n                <span>SKU: ${v.sku}</span>\n                <span>·</span>\n                <span>Made in Germany</span>\n              </div>\n              <h1 class=\"text-foreground text-2xl font-bold tracking-tight sm:text-3xl\">\n                Apex Pro Reference Planar Studio Headphones\n              </h1>\n              <div class=\"flex items-center gap-2 pt-1 text-xs\">\n                <div class=\"text-warning flex items-center gap-1\">\n                  ${icon(Star, 'star', 'fill-warning size-3.5')}\n                  ${icon(Star, 'star', 'fill-warning size-3.5')}\n                  ${icon(Star, 'star', 'fill-warning size-3.5')}\n                  ${icon(Star, 'star', 'fill-warning size-3.5')}\n                  ${icon(Star, 'star', 'fill-warning size-3.5')}\n                </div>\n                <span class=\"text-foreground font-semibold\">4.9</span>\n                <span class=\"text-muted-foreground\">(342 Verified Mastering Reviews)</span>\n              </div>\n            </div>\n\n            <uip-separator></uip-separator>\n\n            <div class=\"space-y-1.5\">\n              <div class=\"flex items-baseline gap-3\">\n                <span class=\"text-foreground text-3xl font-extrabold tracking-tight\"> $${v.price} </span>\n                <span class=\"text-muted-foreground text-base line-through\"> $${v.originalPrice} </span>\n                <uip-badge variant=\"secondary\" class=\"text-success font-mono text-xs font-bold\">\n                  Save $${v.originalPrice - v.price} (20% OFF)\n                </uip-badge>\n              </div>\n              <p class=\"text-muted-foreground text-xs\">\n                Interest-free installments of $${(v.price / 4).toFixed(2)}/mo with Affirm or Klarna.\n              </p>\n            </div>\n\n            <div class=\"space-y-2.5\">\n              <div class=\"flex items-center justify-between text-xs\">\n                <span class=\"text-foreground font-medium\">Finish / Enclosure:</span>\n                <span class=\"text-muted-foreground font-mono font-semibold\">${v.name}</span>\n              </div>\n              <div class=\"flex items-center gap-3\">\n                ${productVariants.map(\n                  (opt) => html`<button\n                    type=\"button\"\n                    aria-label=${opt.name}\n                    class=${`group relative flex size-9 items-center justify-center rounded-full border transition-colors ${\n                      this.selectedVariantId === opt.id\n                        ? 'border-primary ring-primary ring-offset-background ring-2 ring-offset-2'\n                        : 'border-border hover:border-muted-foreground'\n                    }`}\n                    @click=${() => this.selectVariant(opt)}\n                  >\n                    <span class=\"size-6 rounded-full border border-black/10 shadow-2xs\" style=\"background-color:${opt.colorHex}\"></span>\n                  </button>`,\n                )}\n              </div>\n            </div>\n\n            <div class=\"space-y-2.5\">\n              <div class=\"flex items-center justify-between text-xs\">\n                <span class=\"text-foreground font-medium\">Primary Studio Cable Termination:</span>\n                <span class=\"text-primary font-mono text-xs\">Included in package</span>\n              </div>\n              <div class=\"grid grid-cols-3 gap-2 text-xs\">\n                <button\n                  type=\"button\"\n                  class=${`rounded-lg border p-2.5 text-center transition-colors ${\n                    this.selectedCable === '4.4mm'\n                      ? 'border-primary bg-primary/5 text-primary ring-primary font-semibold ring-1'\n                      : 'border-border bg-card text-foreground hover:bg-muted/40'\n                  }`}\n                  @click=${() => {\n                    this.selectedCable = '4.4mm'\n                  }}\n                >\n                  <div class=\"font-mono text-xs font-semibold\">4.4mm Balanced</div>\n                  <div class=\"text-muted-foreground text-xs\">Pentaconn TRRRS</div>\n                </button>\n                <button\n                  type=\"button\"\n                  class=${`rounded-lg border p-2.5 text-center transition-colors ${\n                    this.selectedCable === '3.5mm'\n                      ? 'border-primary bg-primary/5 text-primary ring-primary font-semibold ring-1'\n                      : 'border-border bg-card text-foreground hover:bg-muted/40'\n                  }`}\n                  @click=${() => {\n                    this.selectedCable = '3.5mm'\n                  }}\n                >\n                  <div class=\"font-mono text-xs font-semibold\">3.5mm SE + 6.35mm</div>\n                  <div class=\"text-muted-foreground text-xs\">Screw Adapter</div>\n                </button>\n                <button\n                  type=\"button\"\n                  class=${`rounded-lg border p-2.5 text-center transition-colors ${\n                    this.selectedCable === 'xlr'\n                      ? 'border-primary bg-primary/5 text-primary ring-primary font-semibold ring-1'\n                      : 'border-border bg-card text-foreground hover:bg-muted/40'\n                  }`}\n                  @click=${() => {\n                    this.selectedCable = 'xlr'\n                  }}\n                >\n                  <div class=\"font-mono text-xs font-semibold\">4-Pin XLR Pro</div>\n                  <div class=\"text-muted-foreground text-xs\">Neutrik Studio</div>\n                </button>\n              </div>\n            </div>\n\n            <label\n              class=\"border-border bg-muted/20 hover:bg-muted/30 flex cursor-pointer items-start gap-3 rounded-lg border p-3 text-xs transition-colors\"\n            >\n              <input\n                type=\"checkbox\"\n                ?checked=${this.addExtendedWarranty}\n                class=\"border-border text-primary focus:ring-primary mt-0.5 size-4 rounded\"\n                @change=${(e: Event) => {\n                  this.addExtendedWarranty = (e.target as HTMLInputElement).checked\n                }}\n              />\n              <div class=\"space-y-0.5\">\n                <div class=\"flex items-center justify-between\">\n                  <span class=\"text-foreground font-semibold\">Add 3-Year Apex Care+ Accidental Protection</span>\n                  <span class=\"text-foreground font-mono font-bold\">+$49.00</span>\n                </div>\n                <p class=\"text-muted-foreground text-xs\">Covers accidental drops, driver voice-coil failure, and free earpad replacement.</p>\n              </div>\n            </label>\n\n            <div class=\"space-y-1.5 rounded-lg border border-orange-500/30 bg-orange-500/5 p-3 text-xs\">\n              <div class=\"flex items-center justify-between\">\n                <div class=\"flex items-center gap-1.5 font-semibold text-orange-600 dark:text-orange-400\">\n                  ${icon(Flame, 'flame', 'size-3.5 fill-current')}\n                  <span>Fulfillment Hub Stock Level</span>\n                </div>\n                <span class=\"text-foreground font-mono font-bold\">${v.stockCount} units remaining</span>\n              </div>\n              <p class=\"text-muted-foreground text-xs\">High demand: 18 sound engineers have this item in their active checkout carts.</p>\n            </div>\n\n            <div class=\"space-y-3 pt-2\">\n              <div class=\"flex items-center gap-3\">\n                <div class=\"border-border bg-background flex h-10 items-center rounded-lg border shadow-2xs\">\n                  <button\n                    type=\"button\"\n                    aria-label=\"Decrease quantity\"\n                    class=\"text-muted-foreground hover:text-foreground flex size-9 items-center justify-center\"\n                    @click=${() => this.decrementQty()}\n                  >\n                    ${icon(Minus, 'minus', 'size-3.5')}\n                  </button>\n                  <span class=\"text-foreground w-8 text-center font-mono text-xs font-semibold\">${this.quantity}</span>\n                  <button\n                    type=\"button\"\n                    aria-label=\"Increase quantity\"\n                    class=\"text-muted-foreground hover:text-foreground flex size-9 items-center justify-center\"\n                    @click=${() => this.incrementQty()}\n                  >\n                    ${icon(Plus, 'plus', 'size-3.5')}\n                  </button>\n                </div>\n                <uip-button class=\"h-10 flex-1 gap-2 font-medium shadow-xs\" @click=${() => this.handleAddToCart()}>\n                  ${icon(ShoppingCart, 'shopping-cart', 'size-4')}\n                  <span>Add to Cart · $${this.finalPrice}</span>\n                </uip-button>\n              </div>\n              <uip-button variant=\"secondary\" class=\"border-border h-10 w-full gap-2 border font-semibold shadow-2xs\" @click=${() => this.handleExpressBuy()}>\n                ${icon(Zap, 'zap', 'fill-warning text-warning size-4')}\n                <span>Instant 1-Click Checkout (Apple Pay / G-Pay)</span>\n              </uip-button>\n            </div>\n\n            <div class=\"border-border bg-muted/20 flex items-center gap-2 rounded-lg border p-2.5 text-xs\">\n              ${icon(Truck, 'truck', 'text-muted-foreground size-4 shrink-0')}\n              <span class=\"text-muted-foreground\">Deliver to:</span>\n              <uip-input\n                .value=${this.zipCode}\n                maxlength=\"5\"\n                class=\"h-7 w-20 text-center font-mono text-xs\"\n                @input=${(e: Event) => {\n                  this.zipCode = (e.target as UipInput).value ?? ''\n                }}\n              ></uip-input>\n              <span class=\"text-foreground font-medium\">Fast Delivery: <strong>Tomorrow, 2:00 PM</strong></span>\n            </div>\n          </div>\n        </div>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-product-detail-page') || customElements.define('uip-product-detail-page', UipProductDetailPage)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-product-detail-page': UipProductDetailPage\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/product-detail-page/product-detail-page.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/badge.json",
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/input.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/separator.json"
  ]
}
