Lead Capture Popup
blockmarketingHigh-conversion exit-intent discount popup and newsletter lead generator with live ticking countdown timer, emerald value props, star rating social proof, and coupon code reveal.
Installation
$pnpm dlx uipkge-ng@latest add lead-capture-popup$npx uipkge-ng@latest add lead-capture-popup$yarn dlx uipkge-ng@latest add lead-capture-popup$bunx uipkge-ng@latest add lead-capture-popupManifest:
https://uipkge.dev/r/angular/lead-capture-popup.jsonInstalls to:src/app/components/blocks/lead-capture-popup/Variants
Loading interactive previews…
Loading interactive previews…
npm dependencies
Files installed (2)
src/app/components/blocks/lead-capture-popup/lead-capture-popup.component.ts14.2 kB
// High-conversion exit-intent discount popup and newsletter lead generator with a live // ticking countdown timer, value props, star-rating social proof, and coupon code reveal. // Port of the React block 1:1. import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output, signal } from '@angular/core' import { ArrowRight, Check, CheckCircle2, Clock, Copy, Gift, LucideAngularModule, RotateCcw, Star, X, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiInputComponent } from '@/ui/input/input.component' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-lead-capture-popup, [ui-lead-capture-popup]', standalone: true, // The block renders its own root <div> (form vs dismissed fallback): keep the host out of layout. host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiInputComponent], template: ` @if (!isDismissed()) { <div data-slot="lead-capture-popup" [class]="rootClass"> <div class="bg-card text-card-foreground border-border relative mx-auto w-full max-w-lg overflow-hidden rounded-2xl border p-6 shadow-xl sm:p-8" > <!-- Top Right Dismiss Button --> <button type="button" aria-label="Dismiss popup" class="text-muted-foreground hover:text-foreground focus-visible:ring-ring absolute top-4 right-4 rounded-full p-1.5 transition-colors focus-visible:ring-2 focus-visible:outline-hidden" (click)="handleDismiss()" > <lucide-icon [img]="X" class="size-4" /> </button> <!-- FORM STATE --> @if (!isSubmitted()) { <div class="relative z-10 space-y-6"> <!-- Top Badge --> <div class="flex items-center"> <span ui-badge variant="secondary" class="border-primary/20 bg-primary/10 text-primary inline-flex items-center gap-1.5 px-3 py-1 text-xs font-medium" > <lucide-icon [img]="Gift" class="text-primary size-3.5" /> Limited Time Welcome Gift </span> </div> <!-- Urgency Countdown Bar --> <div class="border-warning/20 bg-warning/10 text-foreground text-warning flex items-center justify-between gap-2 rounded-lg border px-3.5 py-2 text-xs" > <div class="flex items-center gap-2"> <lucide-icon [img]="Clock" class="text-warning size-4 shrink-0" /> <span class="font-medium" >Claim your {{ discountPercent }}% discount code before the timer expires:</span > </div> <span class="bg-warning/20 text-foreground dark:text-foreground rounded px-2 py-0.5 font-mono text-xs font-bold tabular-nums" > {{ formattedTime() }} </span> </div> <!-- Headline & Subtitle --> <div class="space-y-2"> <h2 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl"> {{ headline }} </h2> <p class="text-muted-foreground text-sm leading-relaxed"> {{ subtitle }} </p> </div> <!-- Value Bullet Points (3 items with success checkmarks) --> <ul class="space-y-2.5" role="list"> <li class="text-muted-foreground flex items-center gap-2.5 text-xs sm:text-sm"> <span class="bg-success/15 text-success flex size-4 shrink-0 items-center justify-center rounded-full" > <lucide-icon [img]="Check" class="size-2.5 stroke-[3]" /> </span> <span>No spam ever — high-signal engineering teardowns</span> </li> <li class="text-muted-foreground flex items-center gap-2.5 text-xs sm:text-sm"> <span class="bg-success/15 text-success flex size-4 shrink-0 items-center justify-center rounded-full" > <lucide-icon [img]="Check" class="size-2.5 stroke-[3]" /> </span> <span>1-click unsubscribe anytime with zero friction</span> </li> <li class="text-muted-foreground flex items-center gap-2.5 text-xs sm:text-sm"> <span class="bg-success/15 text-success flex size-4 shrink-0 items-center justify-center rounded-full" > <lucide-icon [img]="Check" class="size-2.5 stroke-[3]" /> </span> <span>Instant coupon delivery straight to your screen & inbox</span> </li> </ul> <!-- Email Capture Form --> <form class="space-y-3 pt-1" (submit)="handleSubmit($event)"> <ui-input [value]="email()" (valueChange)="email.set($event)" type="email" placeholder="engineer@company.com" required autoComplete="email" aria-label="Email address" class="border-border bg-background focus-visible:ring-primary h-11 text-sm shadow-xs focus-visible:ring-2" /> <button ui-button type="submit" size="lg" class="h-11 w-full justify-center gap-2 text-sm font-semibold shadow-xs" > Claim My {{ discountPercent }}% Discount <lucide-icon [img]="ArrowRight" class="size-4" /> </button> </form> <!-- Social Proof Line --> <div class="text-muted-foreground flex items-center justify-center gap-1.5 text-xs"> <div class="text-warning flex items-center gap-0.5" aria-hidden="true"> <lucide-icon [img]="Star" class="size-3.5 fill-current" /> <lucide-icon [img]="Star" class="size-3.5 fill-current" /> <lucide-icon [img]="Star" class="size-3.5 fill-current" /> <lucide-icon [img]="Star" class="size-3.5 fill-current" /> <lucide-icon [img]="Star" class="size-3.5 fill-current" /> </div> <span class="text-foreground font-medium">{{ socialProofRating }}</span> <span>rating by {{ socialProofCount }}</span> </div> <!-- Dismiss / Decline link --> <div class="text-center"> <button type="button" class="text-muted-foreground hover:text-foreground focus-visible:ring-ring min-h-6 rounded text-xs underline underline-offset-4 transition-colors focus-visible:ring-2 focus-visible:outline-hidden" (click)="handleDismiss()" > No thanks, I prefer paying full price </button> </div> </div> } @else { <!-- SUCCESS STATE --> <div class="relative z-10 space-y-5 text-center"> <div class="bg-success/10 text-success mx-auto flex size-12 items-center justify-center rounded-full"> <lucide-icon [img]="CheckCircle2" class="size-6" /> </div> <div class="space-y-1.5"> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success font-medium"> {{ discountPercent }}% Discount Unlocked </span> <h3 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">You’re all set!</h3> <p class="text-muted-foreground text-xs sm:text-sm"> We’ve dispatched your code and welcome gift to <span class="text-foreground font-medium">{{ email() || 'engineer@company.com' }}</span >. </p> </div> <!-- Coupon Card Box --> <div class="border-border bg-muted/40 relative space-y-3 rounded-xl border-2 border-dashed p-4"> <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Your Exclusive Coupon Code </span> <div class="flex items-center justify-center gap-2"> <code class="border-border bg-background text-foreground rounded-lg border px-3 py-1.5 font-mono text-xl font-bold tracking-widest sm:text-2xl" > {{ discountCode }} </code> <button ui-button variant="secondary" size="sm" class="gap-1.5 shadow-xs" [attr.aria-label]="copied() ? 'Copied code' : 'Copy discount code'" (click)="copyCoupon()" > @if (copied()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Copy" class="size-3.5" /> } <span>{{ copied() ? 'Copied!' : 'Copy Code' }}</span> </button> </div> <p class="text-muted-foreground text-xs"> Valid for the next <span class="font-mono font-medium">{{ formattedTime() }}</span> at checkout. </p> </div> <div class="space-y-2 pt-2"> <button ui-button class="h-11 w-full justify-center gap-2 text-sm font-semibold shadow-xs" (click)="handleDismiss()" > Start Exploring Components <lucide-icon [img]="ArrowRight" class="size-4" /> </button> <button type="button" class="text-muted-foreground hover:text-foreground focus-visible:ring-ring min-h-6 rounded text-xs underline underline-offset-4 transition-colors focus-visible:ring-2 focus-visible:outline-hidden" (click)="handleReset()" > Enter a different email </button> </div> </div> } </div> </div> } @else { <!-- Reopen trigger fallback when dismissed --> <div class="flex flex-col items-center justify-center p-8 text-center" data-slot="lead-capture-popup-dismissed"> <p class="text-muted-foreground text-sm">Popup was dismissed.</p> <button ui-button variant="outline" size="sm" class="mt-3 gap-2" (click)="handleReset()"> <lucide-icon [img]="RotateCcw" class="text-primary size-3.5" /> Reopen Welcome Popup </button> </div> } `, }) export class UiLeadCapturePopupComponent implements OnInit, OnDestroy { protected readonly ArrowRight = ArrowRight protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly Copy = Copy protected readonly Gift = Gift protected readonly RotateCcw = RotateCcw protected readonly Star = Star protected readonly X = X @Input() headline = 'Unlock 20% Off Your First Order' @Input() subtitle = 'Join 45,000+ engineers receiving our weekly curated component teardowns and architectural breakdowns.' @Input() discountCode = 'WELCOME20' @Input() discountPercent = 20 @Input() initialSeconds = 599 @Input() socialProofRating = '4.9/5' @Input() socialProofCount = '1,200+ developers' @Input() initialSubmitted = false @Input('class') className?: string @Output() readonly submit = new EventEmitter<{ email: string; discountCode: string }>() @Output() readonly dismiss = new EventEmitter<void>() @Output() readonly copy = new EventEmitter<string>() @Output('reset') readonly resetEmitter = new EventEmitter<void>() readonly email = signal('') readonly isSubmitted = signal(false) readonly isDismissed = signal(false) readonly copied = signal(false) readonly timeLeft = signal(599) private timer: ReturnType<typeof setInterval> | null = null private copyTimer: ReturnType<typeof setTimeout> | null = null ngOnInit(): void { this.isSubmitted.set(this.initialSubmitted) this.timeLeft.set(this.initialSeconds) this.timer = setInterval(() => { if (this.timeLeft() > 0) this.timeLeft.update((v) => v - 1) else if (this.timer) clearInterval(this.timer) }, 1000) } ngOnDestroy(): void { if (this.timer) clearInterval(this.timer) if (this.copyTimer) clearTimeout(this.copyTimer) } get rootClass(): string { return cn('w-full', this.className) } formattedTime(): string { const mins = Math.floor(this.timeLeft() / 60) const secs = this.timeLeft() % 60 return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}` } handleSubmit(event?: Event): void { event?.preventDefault() const value = this.email() if (!value || !value.includes('@')) return this.isSubmitted.set(true) this.submit.emit({ email: value, discountCode: this.discountCode }) } handleDismiss(): void { this.isDismissed.set(true) this.dismiss.emit() } handleReset(): void { this.isSubmitted.set(false) this.isDismissed.set(false) this.email.set('') this.copied.set(false) this.timeLeft.set(this.initialSeconds) this.resetEmitter.emit() } open(): void { this.isDismissed.set(false) } reset(): void { this.handleReset() } copyCoupon(): void { try { void navigator.clipboard?.writeText(this.discountCode) } catch { // Clipboard fallback } this.copied.set(true) this.copy.emit(this.discountCode) if (this.copyTimer) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => this.copied.set(false), 2000) } }src/app/components/blocks/lead-capture-popup/index.ts0.1 kB
export { UiLeadCapturePopupComponent } from './lead-capture-popup.component'
Raw manifest:https://uipkge.dev/r/angular/lead-capture-popup.json