{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "frequently-bought-together",
  "title": "Frequently Bought Together",
  "type": "registry:block",
  "description": "E-commerce frequently bought together bundle builder with interactive product preview cards, checkbox selection, dynamic bundle discount calculation, and one-click cart checkout.",
  "categories": [
    "commerce",
    "ecommerce"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/frequently-bought-together/frequently-bought-together.ts",
      "content": "import { LitElement, css, html } from 'lit'\nimport { Plus, ShoppingBag, Truck } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect imports: register the composed elements for standalone use\n// (consumers who install only this block still get working primitives).\nimport '../../components/badge/badge'\nimport '../../components/button/button'\nimport '../../components/card/card'\nimport '../../components/checkbox/checkbox'\n\ninterface BundleItem {\n  id: string\n  name: string\n  subtitle: string\n  price: number\n  image: string\n  isMain?: boolean\n}\n\nconst bundleItems: BundleItem[] = [\n  {\n    id: 'headphones',\n    name: 'Pro Studio Headphones',\n    subtitle: 'Space Black · Over-Ear Wireless ANC',\n    price: 299.0,\n    image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=500&auto=format&fit=crop&q=80',\n    isMain: true,\n  },\n  {\n    id: 'case',\n    name: 'Premium Hard Shell Case',\n    subtitle: 'Weather-resistant EVA shell with plush velvet lining',\n    price: 45.0,\n    image: 'https://images.unsplash.com/photo-1583394838336-acd977736f90?w=500&auto=format&fit=crop&q=80',\n  },\n  {\n    id: 'cable',\n    name: 'Braided 3.5mm Audiophile Cable',\n    subtitle: '1.5m Silver-plated OFC copper with gold-plated jacks',\n    price: 25.0,\n    image: 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?w=500&auto=format&fit=crop&q=80',\n  },\n]\n\nfunction formatCurrency(val: number) {\n  return '$' + val.toFixed(2)\n}\n\n/**\n * <uip-frequently-bought-together> — the registry FrequentlyBoughtTogether\n * block as a web component.\n *\n * Class strings and bundle data are React's verbatim. React state becomes a\n * { state: true } field. Like React, the block takes no props and fires no\n * events; selection, discount math, and button/delivery copy are internal.\n */\nexport class UipFrequentlyBoughtTogether 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    selectedIds: { state: true },\n  }\n\n  private selectedIds: string[] = ['headphones', 'case', 'cable']\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', 'frequently-bought-together')\n  }\n\n  private toggleItem(id: string) {\n    this.selectedIds = this.selectedIds.includes(id)\n      ? this.selectedIds.filter((itemId) => itemId !== id)\n      : [...this.selectedIds, id]\n  }\n\n  private get selectedItems() {\n    return bundleItems.filter((item) => this.selectedIds.includes(item.id))\n  }\n\n  private get buttonText() {\n    const n = this.selectedItems.length\n    if (n === bundleItems.length) return `Add all ${bundleItems.length} to Cart`\n    if (n > 1) return `Add ${n} selected to Cart`\n    if (n === 1) return 'Add 1 selected to Cart'\n    return 'Select items to add'\n  }\n\n  private get deliveryNote() {\n    const n = this.selectedItems.length\n    if (n >= 2) return 'Free express delivery included with this bundle'\n    if (n === 1) return 'Standard delivery included'\n    return 'Select items to view delivery options'\n  }\n\n  render() {\n    const selectedCount = this.selectedItems.length\n    const originalTotal = this.selectedItems.reduce((sum, item) => sum + item.price, 0)\n    const discountRate = selectedCount >= 2 ? 0.15 : 0\n    const discountAmount = originalTotal * discountRate\n    const finalPrice = originalTotal - discountAmount\n    const hasDiscount = discountRate > 0\n    return html`\n      <!-- data-slot lives on this wrapper: <uip-card> stamps its own\n           data-slot=\"card\" on connect, which would clobber the block slot. -->\n      <div data-slot=\"frequently-bought-together\" class=\"mx-auto w-full max-w-4xl\">\n        <uip-card class=\"p-6 shadow-xs\">\n          <!-- flex column + gap: space-y-* can't target slotted children\n               (host page preflight wins), so the stack rhythm lives here. -->\n          <div class=\"flex flex-col gap-6\">\n        <div class=\"space-y-1.5\">\n          <h2 class=\"text-foreground text-xl font-bold tracking-tight sm:text-2xl\">Frequently Bought Together</h2>\n          <p class=\"text-muted-foreground text-sm\">\n            Pair with these recommended essentials for maximum performance and save 15% on the bundle\n          </p>\n        </div>\n\n        <div class=\"grid grid-cols-1 items-center gap-3 sm:grid-cols-[1fr_auto_1fr_auto_1fr] sm:gap-4\">\n          ${bundleItems.map(\n            (item, index) => html`${index > 0\n                ? html`<div\n                    class=\"border-border/50 text-muted-foreground bg-muted mx-auto flex size-8 shrink-0 items-center justify-center rounded-full border\"\n                    aria-hidden=\"true\"\n                  >\n                    ${icon(Plus, 'plus', 'size-4')}\n                  </div>`\n                : null}\n              <div\n                class=${cn(\n                  'border-border bg-muted/20 relative flex flex-col rounded-xl border p-3 text-left transition-colors duration-200 sm:p-4',\n                  this.selectedIds.includes(item.id) ? 'opacity-100' : 'opacity-40 grayscale',\n                )}\n              >\n                <div class=\"border-border bg-background relative mb-3 aspect-square w-full overflow-hidden rounded-lg border\">\n                  <img src=${item.image} alt=${item.name} class=\"size-full object-cover\" />\n                  ${item.isMain\n                    ? html`<uip-badge\n                        variant=\"secondary\"\n                        class=\"bg-background/90 text-foreground absolute top-2 left-2 text-xs font-medium backdrop-blur-xs\"\n                      >\n                        This item\n                      </uip-badge>`\n                    : null}\n                </div>\n                <span class=\"text-foreground line-clamp-1 text-xs leading-tight font-semibold sm:text-sm\">${item.name}</span>\n                <span class=\"text-foreground mt-1 text-xs font-semibold tabular-nums sm:text-sm\"\n                  >${formatCurrency(item.price)}</span\n                >\n              </div>`,\n          )}\n        </div>\n\n        <div class=\"space-y-2.5\">\n          ${bundleItems.map(\n            (item) => html`<div\n              role=\"button\"\n              tabindex=\"0\"\n              class=${cn(\n                'border-border focus-visible:ring-ring flex cursor-pointer items-start justify-between gap-3 rounded-lg border p-3.5 transition-colors select-none focus-visible:ring-2 focus-visible:outline-none sm:items-center',\n                this.selectedIds.includes(item.id)\n                  ? 'bg-card hover:border-primary/40'\n                  : 'border-border/60 bg-muted/10 opacity-60 hover:opacity-80',\n              )}\n              @click=${() => this.toggleItem(item.id)}\n              @keydown=${(e: KeyboardEvent) => {\n                if (e.key === ' ' || e.key === 'Enter') {\n                  e.preventDefault()\n                  this.toggleItem(item.id)\n                }\n              }}\n            >\n              <div class=\"flex items-start gap-3 sm:items-center\">\n                <uip-checkbox\n                  .checked=${this.selectedIds.includes(item.id)}\n                  class=\"pointer-events-none mt-0.5 sm:mt-0\"\n                  tabindex=\"-1\"\n                ></uip-checkbox>\n                <div class=\"space-y-0.5\">\n                  <div class=\"flex flex-wrap items-center gap-2\">\n                    <span class=\"text-foreground text-sm font-medium\">\n                      ${item.isMain ? html`<span class=\"font-semibold\">This item: </span>` : null}${item.name}\n                    </span>\n                    ${item.isMain ? html`<uip-badge variant=\"secondary\" class=\"text-xs\">Main Item</uip-badge>` : null}\n                  </div>\n                  <p class=\"text-muted-foreground text-xs\">${item.subtitle}</p>\n                </div>\n              </div>\n              <span class=\"text-foreground shrink-0 text-sm font-semibold tabular-nums\">${formatCurrency(item.price)}</span>\n            </div>`,\n          )}\n        </div>\n\n        <div class=\"border-border bg-muted/30 flex flex-col gap-4 rounded-xl border p-4 sm:p-5 md:flex-row md:items-center md:justify-between\">\n          <div class=\"space-y-1.5\">\n            <div class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">\n              ${selectedCount >= 2 ? 'Bundle Price' : 'Total Price'}\n            </div>\n            <div class=\"flex flex-wrap items-baseline gap-2.5\">\n              <span class=\"text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl\"\n                >${formatCurrency(finalPrice)}</span\n              >\n              ${hasDiscount\n                ? html`<span class=\"text-muted-foreground text-sm font-medium tabular-nums line-through sm:text-base\"\n                    >${formatCurrency(originalTotal)}</span\n                  >`\n                : null}\n              ${hasDiscount\n                ? html`<uip-badge variant=\"outline\" class=\"border-success/30 bg-success/10 text-success text-xs font-medium\">\n                    Save ${formatCurrency(discountAmount)} (15% Bundle Discount)\n                  </uip-badge>`\n                : null}\n            </div>\n            <div class=\"text-muted-foreground flex items-center gap-1.5 text-xs\">\n              ${icon(Truck, 'truck', 'text-success size-3.5 shrink-0')}\n              <span>${this.deliveryNote}</span>\n            </div>\n          </div>\n          <uip-button size=\"lg\" ?disabled=${selectedCount === 0} class=\"w-full gap-2 font-semibold shadow-xs md:w-auto\">\n            ${icon(ShoppingBag, 'shopping-bag', 'size-4')}\n            <span>${this.buttonText}</span>\n          </uip-button>\n          </div>\n        </div>\n      </uip-card>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-frequently-bought-together') ||\n  customElements.define('uip-frequently-bought-together', UipFrequentlyBoughtTogether)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-frequently-bought-together': UipFrequentlyBoughtTogether\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/frequently-bought-together/frequently-bought-together.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/badge.json",
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/card.json",
    "https://uipkge.dev/r/lit/checkbox.json",
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
