{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "pricing-feature-addon-builder",
  "title": "Pricing Feature Addon Builder",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/pricing-feature-addon-builder/pricing-feature-addon-builder.component.ts",
      "content": "// Interactive add-on pricing calculator: tier bundling, seat scaling, and real-time\n// invoice generation with an annual billing toggle. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'\nimport { ArrowRight, Calculator, Check, CreditCard, LucideAngularModule } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component'\n\nexport interface PricingPlan {\n  id: string\n  name: string\n  monthlyPrice: number\n  description: string\n  includedSeats: number\n}\n\nexport interface PricingAddon {\n  id: string\n  name: string\n  monthlyPrice: number\n  description: string\n  category: string\n}\n\nconst DEFAULT_PLANS: PricingPlan[] = [\n  {\n    id: 'starter',\n    name: 'Starter Tier',\n    monthlyPrice: 29,\n    description: 'For indie developers and early-stage prototypes needing unbundled speed.',\n    includedSeats: 2,\n  },\n  {\n    id: 'growth',\n    name: 'Growth Core',\n    monthlyPrice: 99,\n    description: 'For scaling product engineering teams building high-conversion platforms.',\n    includedSeats: 5,\n  },\n  {\n    id: 'scale',\n    name: 'Scale Enterprise',\n    monthlyPrice: 299,\n    description: 'For mission-critical production clusters requiring sub-10ms global edge delivery.',\n    includedSeats: 15,\n  },\n]\n\nconst DEFAULT_ADDONS: PricingAddon[] = [\n  {\n    id: 'dedicated-ip',\n    name: 'Dedicated Static Edge IP',\n    monthlyPrice: 49,\n    description: 'Static IPv4/IPv6 address allocations with zero-reputation penalty.',\n    category: 'Network',\n  },\n  {\n    id: 'audit-logs',\n    name: 'Immutable SOC2 Audit Logs',\n    monthlyPrice: 79,\n    description: 'Cryptographically signed telemetry logs with 365-day cold storage retention.',\n    category: 'Security',\n  },\n  {\n    id: 'multi-region',\n    name: 'Multi-Region Active-Active Mesh',\n    monthlyPrice: 129,\n    description: 'Synchronized cross-continental database replicas and automatic DNS failover.',\n    category: 'Reliability',\n  },\n  {\n    id: 'priority-sla',\n    name: '1-Hour Enterprise Response SLA',\n    monthlyPrice: 199,\n    description: 'Direct Slack / Discord hotline with senior design engineering staff.',\n    category: 'Support',\n  },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-pricing-feature-addon-builder, [ui-pricing-feature-addon-builder]',\n  standalone: true,\n  // React renders the root <section> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent],\n  template: `\n    <section data-slot=\"pricing-feature-addon-builder\" [class]=\"rootClass\">\n      <div class=\"mx-auto max-w-7xl px-4 sm:px-6 lg:px-8\">\n        <!-- Section Header -->\n        <div class=\"mx-auto max-w-3xl space-y-4 text-center\">\n          <a\n            href=\"#pricing-calculator\"\n            class=\"group border-border/80 bg-secondary/60 hover:bg-secondary text-foreground inline-flex items-center gap-2 rounded-full border px-3.5 py-1 text-xs font-medium shadow-2xs transition-colors\"\n          >\n            <lucide-icon [img]=\"Calculator\" class=\"text-primary size-3.5\" />\n            <span>Real-time Add-on Cost Synthesizer</span>\n            <lucide-icon\n              [img]=\"ArrowRight\"\n              class=\"text-muted-foreground size-3 transition-transform group-hover:translate-x-0.5\"\n            />\n          </a>\n\n          <h2 class=\"text-foreground text-3xl font-bold tracking-tight sm:text-4xl\">\n            {{ title() }}\n          </h2>\n\n          <p class=\"text-muted-foreground text-base sm:text-lg\">\n            {{ description() }}\n          </p>\n\n          <!-- Billing Cadence Toggle -->\n          <div class=\"flex items-center justify-center gap-3 pt-2\">\n            <span [class]=\"cadenceLabelClass(!isAnnual())\">Monthly Billing</span>\n            <button\n              type=\"button\"\n              aria-label=\"Toggle annual billing\"\n              [class]=\"cadenceTrackClass()\"\n              (click)=\"isAnnual.set(!isAnnual())\"\n            >\n              <span aria-hidden=\"true\" [class]=\"cadenceThumbClass()\"></span>\n            </button>\n            <span [class]=\"cadenceAnnualLabelClass()\">\n              <span>Annual Billing</span>\n              <span ui-badge variant=\"outline\" class=\"border-success/30 bg-success/10 text-success text-xs\">\n                Save 20%\n              </span>\n            </span>\n          </div>\n        </div>\n\n        <!-- Add-on Builder Workbench -->\n        <div class=\"mt-12 grid grid-cols-1 gap-6 lg:grid-cols-12\">\n          <!-- Left: Plan Selection & Addon Toggles (7 Cols) -->\n          <div class=\"space-y-6 lg:col-span-7\">\n            <!-- Step 1: Base Tier Cards -->\n            <div class=\"space-y-3\">\n              <div class=\"text-muted-foreground text-xs font-bold tracking-wider uppercase\">\n                Step 1: Choose Base Core Tier\n              </div>\n              <div class=\"grid grid-cols-1 gap-3 sm:grid-cols-3\">\n                @for (plan of activePlans(); track plan.id) {\n                  <button type=\"button\" [class]=\"planCardClass(plan.id)\" (click)=\"selectedPlanId.set(plan.id)\">\n                    <div>\n                      <div class=\"text-foreground text-xs font-bold\">{{ plan.name }}</div>\n                      <div class=\"text-foreground mt-1 font-mono text-lg font-bold\">\n                        &#36;{{ plan.monthlyPrice }}<span class=\"text-muted-foreground text-xs font-normal\">/mo</span>\n                      </div>\n                    </div>\n                    <div class=\"text-muted-foreground mt-2 text-xs\">\n                      Includes {{ plan.includedSeats }} engineer seats\n                    </div>\n                  </button>\n                }\n              </div>\n            </div>\n\n            <!-- Step 2: Seat Allocation Slider -->\n            <div ui-card class=\"border-border bg-card/60 shadow-2xs\">\n              <div ui-card-content class=\"space-y-2.5 p-4\">\n                <div class=\"flex items-center justify-between text-xs\">\n                  <span class=\"text-foreground font-bold\">Engineer Team Seats</span>\n                  <span class=\"text-foreground font-mono text-sm font-bold\"\n                    >{{ seatCount() }} seats (&#36;{{ extraSeatCost() }}/mo extra)</span\n                  >\n                </div>\n                <input\n                  type=\"range\"\n                  min=\"2\"\n                  max=\"50\"\n                  step=\"1\"\n                  aria-label=\"Engineer team seats\"\n                  [value]=\"seatCount()\"\n                  (input)=\"onSeatInput($event)\"\n                  class=\"accent-primary w-full cursor-pointer\"\n                />\n                <div class=\"text-muted-foreground flex justify-between font-mono text-xs\">\n                  <span>2 seats</span>\n                  <span>50 seats</span>\n                </div>\n              </div>\n            </div>\n\n            <!-- Step 3: Enterprise Add-ons Checklist -->\n            <div class=\"space-y-3\">\n              <div class=\"text-muted-foreground text-xs font-bold tracking-wider uppercase\">\n                Step 3: Select Modular Capabilities\n              </div>\n              <div class=\"grid grid-cols-1 gap-2.5\">\n                @for (addon of activeAddons(); track addon.id) {\n                  <button type=\"button\" [class]=\"addonCardClass(addon.id)\" (click)=\"toggleAddon(addon.id)\">\n                    <div class=\"flex min-w-0 items-center gap-3\">\n                      <div [class]=\"addonCheckClass(addon.id)\">\n                        @if (isAddonSelected(addon.id)) {\n                          <lucide-icon [img]=\"Check\" class=\"size-3.5\" />\n                        }\n                      </div>\n                      <div class=\"min-w-0 space-y-0.5\">\n                        <div class=\"flex items-center gap-2\">\n                          <span class=\"text-foreground text-xs font-bold\">{{ addon.name }}</span>\n                          <span ui-badge variant=\"outline\" class=\"border-border text-muted-foreground text-xs\">\n                            {{ addon.category }}\n                          </span>\n                        </div>\n                        <div class=\"text-muted-foreground truncate text-xs\">{{ addon.description }}</div>\n                      </div>\n                    </div>\n\n                    <div class=\"text-foreground shrink-0 pl-2 font-mono text-xs font-bold\">\n                      +&#36;{{ addon.monthlyPrice }}<span class=\"text-muted-foreground text-xs font-normal\">/mo</span>\n                    </div>\n                  </button>\n                }\n              </div>\n            </div>\n          </div>\n\n          <!-- Right: Real-time Invoice Estimate Card (5 Cols) -->\n          <div class=\"lg:col-span-5\">\n            <div ui-card class=\"border-border bg-card sticky top-8 shadow-sm\">\n              <div ui-card-content class=\"space-y-6 p-6\">\n                <div class=\"border-border flex items-center justify-between border-b pb-3\">\n                  <div class=\"flex items-center gap-2\">\n                    <lucide-icon [img]=\"CreditCard\" class=\"text-primary size-4\" />\n                    <span class=\"text-foreground text-sm font-semibold\">Estimated Monthly Invoice</span>\n                  </div>\n                  <span ui-badge variant=\"outline\" class=\"border-border text-primary font-mono text-xs\">\n                    {{ isAnnual() ? 'Annualized' : 'Monthly' }}\n                  </span>\n                </div>\n\n                <!-- Price Breakdown List -->\n                <div class=\"space-y-3 text-xs\">\n                  <div class=\"text-muted-foreground flex justify-between\">\n                    <span>{{ selectedPlan().name }}</span>\n                    <span class=\"text-foreground font-mono\">&#36;{{ selectedPlan().monthlyPrice }}.00</span>\n                  </div>\n\n                  @if (extraSeats() > 0) {\n                    <div class=\"text-muted-foreground flex justify-between\">\n                      <span>Extra Seats ({{ extraSeats() }} × &#36;15)</span>\n                      <span class=\"text-foreground font-mono\">&#36;{{ extraSeatCost() }}.00</span>\n                    </div>\n                  }\n\n                  @for (addon of selectedAddons(); track addon.id) {\n                    <div class=\"text-muted-foreground flex justify-between\">\n                      <span class=\"truncate pr-2\">{{ addon.name }}</span>\n                      <span class=\"text-foreground font-mono\">&#36;{{ addon.monthlyPrice }}.00</span>\n                    </div>\n                  }\n\n                  @if (isAnnual()) {\n                    <div class=\"border-border/60 text-success flex justify-between border-t pt-2 font-medium\">\n                      <span>Annual Billing Discount (20%)</span>\n                      <span class=\"font-mono\">−&#36;{{ monthlySubtotal() - finalMonthlyRate() }}.00</span>\n                    </div>\n                  }\n                </div>\n\n                <!-- Total Sum Band -->\n                <div class=\"border-border bg-muted/40 space-y-1 rounded-lg border p-4\">\n                  <div class=\"text-muted-foreground text-xs font-medium\">Net Monthly Investment</div>\n                  <div class=\"flex items-baseline gap-1.5\">\n                    <span class=\"text-foreground font-mono text-3xl font-bold tracking-tight\">\n                      &#36;{{ finalMonthlyRate() }}\n                    </span>\n                    <span class=\"text-muted-foreground font-mono text-xs\">/ month</span>\n                  </div>\n                  @if (isAnnual()) {\n                    <div class=\"text-success text-xs font-medium\">\n                      Billed annually (&#36;{{ finalMonthlyRate() * 12 }}/yr • Save &#36;{{ annualSavings() }}/yr)\n                    </div>\n                  }\n                </div>\n\n                <button ui-button class=\"w-full gap-2 shadow-xs\" size=\"lg\">\n                  <span>Start 14-Day Free Evaluation</span>\n                  <lucide-icon [img]=\"ArrowRight\" class=\"size-4\" />\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  `,\n})\nexport class UiPricingFeatureAddonBuilderComponent {\n  protected readonly ArrowRight = ArrowRight\n  protected readonly Calculator = Calculator\n  protected readonly Check = Check\n  protected readonly CreditCard = CreditCard\n\n  @Input('title') set titleInput(v: string) {\n    if (v !== undefined) this.titleSignal.set(v)\n  }\n  @Input('description') set descriptionInput(v: string) {\n    if (v !== undefined) this.descriptionSignal.set(v)\n  }\n  @Input('plans') set plansInput(v: PricingPlan[] | undefined) {\n    this.plansSignal.set(v)\n  }\n  @Input('addons') set addonsInput(v: PricingAddon[] | undefined) {\n    this.addonsSignal.set(v)\n  }\n  @Input('class') className?: string\n\n  private readonly titleSignal = signal('Build your custom plan with transparent, zero-surprise pricing.')\n  private readonly descriptionSignal = signal(\n    'Select your base tier, adjust seat allocations, and toggle modular enterprise add-ons with real-time invoice calculations.',\n  )\n  private readonly plansSignal = signal<PricingPlan[] | undefined>(undefined)\n  private readonly addonsSignal = signal<PricingAddon[] | undefined>(undefined)\n\n  readonly title = computed(() => this.titleSignal())\n  readonly description = computed(() => this.descriptionSignal())\n  readonly activePlans = computed(() => this.plansSignal() ?? DEFAULT_PLANS)\n  readonly activeAddons = computed(() => this.addonsSignal() ?? DEFAULT_ADDONS)\n\n  readonly selectedPlanId = signal('growth')\n  readonly selectedAddonIds = signal<string[]>(['dedicated-ip', 'audit-logs'])\n  readonly seatCount = signal(8)\n  readonly isAnnual = signal(true)\n\n  readonly selectedPlan = computed(() => {\n    return this.activePlans().find((p) => p.id === this.selectedPlanId()) ?? this.activePlans()[0]!\n  })\n\n  readonly extraSeats = computed(() => Math.max(0, this.seatCount() - this.selectedPlan().includedSeats))\n  readonly extraSeatCost = computed(() => this.extraSeats() * 15)\n\n  readonly selectedAddons = computed(() => this.activeAddons().filter((a) => this.selectedAddonIds().includes(a.id)))\n\n  readonly totalAddonsCost = computed(() => this.selectedAddons().reduce((sum, a) => sum + a.monthlyPrice, 0))\n\n  readonly monthlySubtotal = computed(\n    () => this.selectedPlan().monthlyPrice + this.extraSeatCost() + this.totalAddonsCost(),\n  )\n\n  readonly finalMonthlyRate = computed(() =>\n    this.isAnnual() ? Math.round(this.monthlySubtotal() * 0.8) : this.monthlySubtotal(),\n  )\n\n  readonly annualSavings = computed(() => (this.monthlySubtotal() - Math.round(this.monthlySubtotal() * 0.8)) * 12)\n\n  get rootClass(): string {\n    return cn('bg-background relative overflow-hidden py-16 sm:py-24', this.className)\n  }\n\n  cadenceLabelClass(active: boolean): string {\n    return cn('text-xs font-medium', active ? 'text-foreground font-semibold' : 'text-muted-foreground')\n  }\n\n  cadenceAnnualLabelClass(): string {\n    return cn(\n      'flex items-center gap-1.5 text-xs font-medium',\n      this.isAnnual() ? 'text-foreground font-semibold' : 'text-muted-foreground',\n    )\n  }\n\n  cadenceTrackClass(): string {\n    return cn(\n      'bg-muted focus-visible:ring-ring relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus-visible:ring-2 focus-visible:outline-none',\n      this.isAnnual() ? 'bg-primary' : 'bg-muted',\n    )\n  }\n\n  cadenceThumbClass(): string {\n    return cn(\n      'bg-background pointer-events-none inline-block size-5 transform rounded-full shadow-lg ring-0 transition duration-200 ease-in-out',\n      this.isAnnual() ? 'translate-x-5' : 'translate-x-0',\n    )\n  }\n\n  planCardClass(planId: string): string {\n    return cn(\n      'flex flex-col justify-between rounded-xl border p-4 text-left transition-colors',\n      this.selectedPlanId() === planId\n        ? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1'\n        : 'border-border bg-card hover:bg-muted/40 text-muted-foreground hover:text-foreground',\n    )\n  }\n\n  addonCardClass(addonId: string): string {\n    return cn(\n      'flex items-center justify-between rounded-xl border p-3.5 text-left transition-colors',\n      this.isAddonSelected(addonId)\n        ? 'border-primary/60 bg-primary/5 shadow-2xs'\n        : 'border-border bg-card hover:bg-muted/30 text-muted-foreground hover:text-foreground',\n    )\n  }\n\n  addonCheckClass(addonId: string): string {\n    return cn(\n      'flex size-5 shrink-0 items-center justify-center rounded border transition-colors',\n      this.isAddonSelected(addonId)\n        ? 'border-primary bg-primary text-primary-foreground'\n        : 'border-border bg-background',\n    )\n  }\n\n  isAddonSelected(addonId: string): boolean {\n    return this.selectedAddonIds().includes(addonId)\n  }\n\n  toggleAddon(addonId: string): void {\n    if (this.isAddonSelected(addonId)) {\n      this.selectedAddonIds.set(this.selectedAddonIds().filter((id) => id !== addonId))\n    } else {\n      this.selectedAddonIds.set([...this.selectedAddonIds(), addonId])\n    }\n  }\n\n  onSeatInput(event: Event): void {\n    const value = Number((event.target as HTMLInputElement).value)\n    if (Number.isFinite(value)) this.seatCount.set(value)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/pricing-feature-addon-builder/pricing-feature-addon-builder.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/pricing-feature-addon-builder/index.ts",
      "content": "export {\n  UiPricingFeatureAddonBuilderComponent,\n  type PricingPlan,\n  type PricingAddon,\n} from './pricing-feature-addon-builder.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/pricing-feature-addon-builder/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"
  ],
  "description": "Interactive add-on pricing calculator with tier bundling, seat scaling, and real-time invoice generation.",
  "categories": [
    "pricing",
    "marketing"
  ]
}