{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "frequently-bought-together",
  "title": "Frequently Bought Together",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/frequently-bought-together/frequently-bought-together.component.ts",
      "content": "// Edit this file to change the bundle items and copy. State is local -- wire the cart button\n// to your own cart API.\nimport { Component, computed, signal, ChangeDetectionStrategy } from '@angular/core'\nimport { LucideAngularModule, Plus, ShoppingBag, Truck } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiCardComponent } from '@/ui/card/card.component'\nimport { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component'\n\nexport interface FrequentlyBoughtTogetherItem {\n  id: string\n  name: string\n  subtitle: string\n  price: number\n  image: string\n  isMain?: boolean\n}\n\nconst items: FrequentlyBoughtTogetherItem[] = [\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): string {\n  return '$' + val.toFixed(2)\n}\n\n/**\n * FrequentlyBoughtTogether -- the React block, 1:1: a bundle builder card with a product\n * preview row (deselected items fade to grayscale), a checkbox list of the items, and a\n * summary box with a live 15% bundle discount (two or more items), the delivery note and\n * an \"Add to Cart\" button whose label follows the selection.\n *\n * lucide-angular wraps each <svg> in a <lucide-icon> host, so the Button's `has-[>svg]:px-4`\n * never matches: the cart button carries the `px-4` React's size=\"lg\" resolves to.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-frequently-bought-together, [ui-frequently-bought-together]',\n  standalone: true,\n  // React renders the Card itself: the host stays out of layout.\n  host: { class: 'contents' },\n  imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCheckboxComponent],\n  template: `\n    <div ui-card data-slot=\"frequently-bought-together\" class=\"mx-auto w-full max-w-4xl space-y-6 p-6 shadow-xs\">\n      <!-- Header -->\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      <!-- Product Visual Row -->\n      <div class=\"grid grid-cols-1 items-center gap-3 sm:grid-cols-[1fr_auto_1fr_auto_1fr] sm:gap-4\">\n        @for (item of items; track item.id; let index = $index) {\n          <div [class]=\"previewClass(item.id)\">\n            <div\n              class=\"border-border bg-background relative mb-3 aspect-square w-full overflow-hidden rounded-lg border\"\n            >\n              <img [src]=\"item.image\" [alt]=\"item.name\" class=\"size-full object-cover\" />\n              @if (item.isMain) {\n                <span\n                  ui-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                </span>\n              }\n            </div>\n            <span class=\"text-foreground line-clamp-1 text-xs leading-tight font-semibold sm:text-sm\">\n              {{ item.name }}\n            </span>\n            <span class=\"text-foreground mt-1 text-xs font-semibold tabular-nums sm:text-sm\">\n              {{ formatCurrency(item.price) }}\n            </span>\n          </div>\n\n          @if (index < items.length - 1) {\n            <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              <lucide-icon [img]=\"Plus\" class=\"size-4\" />\n            </div>\n          }\n        }\n      </div>\n\n      <!-- Item Selection Checkbox List -->\n      <div class=\"space-y-2.5\">\n        @for (item of items; track item.id) {\n          <div\n            role=\"button\"\n            tabindex=\"0\"\n            [class]=\"rowClass(item.id)\"\n            (click)=\"toggleItem(item.id)\"\n            (keydown)=\"onRowKeydown($event, item.id)\"\n          >\n            <div class=\"flex items-start gap-3 sm:items-center\">\n              <ui-checkbox\n                [id]=\"item.id\"\n                [checked]=\"isSelected(item.id)\"\n                class=\"pointer-events-none mt-0.5 sm:mt-0\"\n                tabindex=\"-1\"\n              />\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                    @if (item.isMain) {\n                      <span class=\"font-semibold\">This item: </span>\n                    }\n                    {{ item.name }}\n                  </span>\n                  @if (item.isMain) {\n                    <span ui-badge variant=\"secondary\" class=\"text-xs\">Main Item</span>\n                  }\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\">\n              {{ formatCurrency(item.price) }}\n            </span>\n          </div>\n        }\n      </div>\n\n      <!-- Price & Bundle Checkout Summary Box -->\n      <div\n        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      >\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()) }}\n            </span>\n            @if (hasDiscount()) {\n              <span class=\"text-muted-foreground text-sm font-medium tabular-nums line-through sm:text-base\">\n                {{ formatCurrency(originalTotal()) }}\n              </span>\n            }\n            @if (hasDiscount()) {\n              <span ui-badge variant=\"outline\" class=\"border-success/30 bg-success/10 text-success text-xs font-medium\">\n                Save {{ formatCurrency(discountAmount()) }} (15% Bundle Discount)\n              </span>\n            }\n          </div>\n          <div class=\"text-muted-foreground flex items-center gap-1.5 text-xs\">\n            <lucide-icon [img]=\"Truck\" class=\"text-success size-3.5 shrink-0\" />\n            <span>{{ deliveryNote() }}</span>\n          </div>\n        </div>\n\n        <button\n          ui-button\n          size=\"lg\"\n          [disabled]=\"selectedCount() === 0\"\n          class=\"w-full gap-2 px-4 font-semibold shadow-xs md:w-auto\"\n        >\n          <lucide-icon [img]=\"ShoppingBag\" class=\"size-4\" />\n          <span>{{ buttonText() }}</span>\n        </button>\n      </div>\n    </div>\n  `,\n})\nexport class UiFrequentlyBoughtTogetherComponent {\n  protected readonly Plus = Plus\n  protected readonly ShoppingBag = ShoppingBag\n  protected readonly Truck = Truck\n  protected readonly items = items\n  protected readonly formatCurrency = formatCurrency\n\n  readonly selectedIds = signal<string[]>(['headphones', 'case', 'cable'])\n\n  readonly selectedItems = computed(() => items.filter((item) => this.selectedIds().includes(item.id)))\n  readonly selectedCount = computed(() => this.selectedItems().length)\n  readonly originalTotal = computed(() => this.selectedItems().reduce((sum, item) => sum + item.price, 0))\n  readonly discountRate = computed(() => (this.selectedCount() >= 2 ? 0.15 : 0))\n  readonly discountAmount = computed(() => this.originalTotal() * this.discountRate())\n  readonly finalPrice = computed(() => this.originalTotal() - this.discountAmount())\n  readonly hasDiscount = computed(() => this.discountRate() > 0)\n\n  readonly buttonText = computed(() => {\n    const count = this.selectedCount()\n    if (count === items.length) return `Add all ${items.length} to Cart`\n    if (count > 1) return `Add ${count} selected to Cart`\n    if (count === 1) return 'Add 1 selected to Cart'\n    return 'Select items to add'\n  })\n\n  readonly deliveryNote = computed(() => {\n    const count = this.selectedCount()\n    if (count >= 2) return 'Free express delivery included with this bundle'\n    if (count === 1) return 'Standard delivery included'\n    return 'Select items to view delivery options'\n  })\n\n  isSelected(id: string): boolean {\n    return this.selectedIds().includes(id)\n  }\n\n  toggleItem(id: string): void {\n    this.selectedIds.update((prev) => (prev.includes(id) ? prev.filter((itemId) => itemId !== id) : [...prev, id]))\n  }\n\n  onRowKeydown(event: KeyboardEvent, id: string): void {\n    if (event.key === ' ' || event.key === 'Enter') {\n      event.preventDefault()\n      this.toggleItem(id)\n    }\n  }\n\n  previewClass(id: string): string {\n    return 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.isSelected(id) ? 'opacity-100' : 'opacity-40 grayscale',\n    )\n  }\n\n  rowClass(id: string): string {\n    return 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.isSelected(id)\n        ? 'bg-card hover:border-primary/40'\n        : 'border-border/60 bg-muted/10 opacity-60 hover:opacity-80',\n    )\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/frequently-bought-together/frequently-bought-together.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/frequently-bought-together/index.ts",
      "content": "export {\n  UiFrequentlyBoughtTogetherComponent,\n  type FrequentlyBoughtTogetherItem,\n} from './frequently-bought-together.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/frequently-bought-together/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/checkbox.json"
  ],
  "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"
  ]
}