
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
High-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.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/lead-capture-popup.json$npx shadcn@latest add https://uipkge.dev/r/react/lead-capture-popup.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/lead-capture-popup.json$bunx shadcn@latest add https://uipkge.dev/r/react/lead-capture-popup.jsonnpx shadcn@latest add @uipkge-react/lead-capture-popupInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
headline | string | — | optional |
subtitle | string | — | optional |
discountCode | string | — | optional |
discountPercent | number | — | optional |
initialSeconds | number | — | optional |
socialProofRating | string | — | optional |
socialProofCount | string | — | optional |
initialSubmitted | boolean | — | optional |
className | string | — | optional |
onSubmit | (payload: { email: string; discountCode: string }) => void | — | optional |
onDismiss | () => void | — | optional |
onCopy | (code: string) => void | — | optional |
onReset | () => void | — | optional |
'use client'
import { useEffect, useState } from 'react'
import { ArrowRight, Check, CheckCircle2, Clock, Copy, Gift, RotateCcw, Star, X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
interface LeadCapturePopupProps {
headline?: string
subtitle?: string
discountCode?: string
discountPercent?: number
initialSeconds?: number
socialProofRating?: string
socialProofCount?: string
initialSubmitted?: boolean
className?: string
onSubmit?: (payload: { email: string; discountCode: string }) => void
onDismiss?: () => void
onCopy?: (code: string) => void
onReset?: () => void
}
export function LeadCapturePopup({
headline = 'Unlock 20% Off Your First Order',
subtitle = 'Join 45,000+ engineers receiving our weekly curated component teardowns and architectural breakdowns.',
discountCode = 'WELCOME20',
discountPercent = 20,
initialSeconds = 599,
socialProofRating = '4.9/5',
socialProofCount = '1,200+ developers',
initialSubmitted = false,
className,
onSubmit,
onDismiss,
onCopy,
onReset,
}: LeadCapturePopupProps) {
const [email, setEmail] = useState('')
const [isSubmitted, setIsSubmitted] = useState(initialSubmitted)
const [isDismissed, setIsDismissed] = useState(false)
const [copied, setCopied] = useState(false)
const [timeLeft, setTimeLeft] = useState(initialSeconds)
useEffect(() => {
const timer = setInterval(() => {
setTimeLeft((prev) => (prev > 0 ? prev - 1 : 0))
}, 1000)
return () => clearInterval(timer)
}, [])
const mins = Math.floor(timeLeft / 60)
const secs = timeLeft % 60
const formattedTime = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (!email || !email.includes('@')) return
setIsSubmitted(true)
onSubmit?.({ email, discountCode })
}
function handleDismiss() {
setIsDismissed(true)
onDismiss?.()
}
function handleReset() {
setIsSubmitted(false)
setIsDismissed(false)
setEmail('')
setCopied(false)
setTimeLeft(initialSeconds)
onReset?.()
}
function copyCoupon() {
navigator.clipboard?.writeText(discountCode)
setCopied(true)
onCopy?.(discountCode)
setTimeout(() => {
setCopied(false)
}, 2000)
}
if (isDismissed) {
return (
<div
className="flex flex-col items-center justify-center p-8 text-center"
data-slot="lead-capture-popup-dismissed"
>
<p className="text-muted-foreground text-sm">Popup was dismissed.</p>
<Button variant="outline" size="sm" className="mt-3 gap-2" onClick={handleReset}>
<RotateCcw className="text-primary size-3.5" />
Reopen Welcome Popup
</Button>
</div>
)
}
return (
<div data-slot="lead-capture-popup" className={cn('w-full', className)}>
<div className="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"
className="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"
onClick={handleDismiss}
>
<X className="size-4" />
</button>
{/* FORM STATE */}
{!isSubmitted ? (
<div className="relative z-10 space-y-6">
{/* Top Badge */}
<div className="flex items-center">
<Badge
variant="secondary"
className="border-primary/20 bg-primary/10 text-primary inline-flex items-center gap-1.5 px-3 py-1 text-xs font-medium"
>
<Gift className="text-primary size-3.5" />
Limited Time Welcome Gift
</Badge>
</div>
{/* Urgency Countdown Bar */}
<div className="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 className="flex items-center gap-2">
<Clock className="text-warning size-4 shrink-0" />
<span className="font-medium">
Claim your {discountPercent}% discount code before the timer expires:
</span>
</div>
<span className="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 className="space-y-2">
<h2 className="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">{headline}</h2>
<p className="text-muted-foreground text-sm leading-relaxed">{subtitle}</p>
</div>
{/* Value Bullet Points (3 items with emerald checkmarks) */}
<ul className="space-y-2.5" role="list">
<li className="text-muted-foreground flex items-center gap-2.5 text-xs sm:text-sm">
<span className="bg-success/15 text-success flex size-4 shrink-0 items-center justify-center rounded-full">
<Check className="size-2.5 stroke-[3]" />
</span>
<span>No spam ever — high-signal engineering teardowns</span>
</li>
<li className="text-muted-foreground flex items-center gap-2.5 text-xs sm:text-sm">
<span className="bg-success/15 text-success flex size-4 shrink-0 items-center justify-center rounded-full">
<Check className="size-2.5 stroke-[3]" />
</span>
<span>1-click unsubscribe anytime with zero friction</span>
</li>
<li className="text-muted-foreground flex items-center gap-2.5 text-xs sm:text-sm">
<span className="bg-success/15 text-success flex size-4 shrink-0 items-center justify-center rounded-full">
<Check className="size-2.5 stroke-[3]" />
</span>
<span>Instant coupon delivery straight to your screen & inbox</span>
</li>
</ul>
{/* Email Capture Form */}
<form className="space-y-3 pt-1" onSubmit={handleSubmit}>
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="[email protected]"
required
autoComplete="email"
aria-label="Email address"
className="border-border bg-background focus-visible:ring-primary h-11 text-sm shadow-xs focus-visible:ring-2"
/>
<Button
type="submit"
size="lg"
className="h-11 w-full justify-center gap-2 text-sm font-semibold shadow-xs"
>
Claim My {discountPercent}% Discount
<ArrowRight className="size-4" />
</Button>
</form>
{/* Social Proof Line */}
<div className="text-muted-foreground flex items-center justify-center gap-1.5 text-xs">
<div className="text-warning flex items-center gap-0.5" aria-hidden="true">
<Star className="size-3.5 fill-current" />
<Star className="size-3.5 fill-current" />
<Star className="size-3.5 fill-current" />
<Star className="size-3.5 fill-current" />
<Star className="size-3.5 fill-current" />
</div>
<span className="text-foreground font-medium">{socialProofRating}</span>
<span>rating by {socialProofCount}</span>
</div>
{/* Dismiss / Decline link */}
<div className="text-center">
<button
type="button"
className="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"
onClick={handleDismiss}
>
No thanks, I prefer paying full price
</button>
</div>
</div>
) : (
/* SUCCESS STATE */
<div className="relative z-10 space-y-5 text-center">
<div className="bg-success/10 text-success mx-auto flex size-12 items-center justify-center rounded-full">
<CheckCircle2 className="size-6" />
</div>
<div className="space-y-1.5">
<Badge variant="outline" className="border-success/30 bg-success/10 text-success font-medium">
{discountPercent}% Discount Unlocked
</Badge>
<h3 className="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">You’re all set!</h3>
<p className="text-muted-foreground text-xs sm:text-sm">
We’ve dispatched your code and welcome gift to{' '}
<span className="text-foreground font-medium">{email || '[email protected]'}</span>.
</p>
</div>
{/* Coupon Card Box */}
<div className="border-border bg-muted/40 relative space-y-3 rounded-xl border-2 border-dashed p-4">
<span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Your Exclusive Coupon Code
</span>
<div className="flex items-center justify-center gap-2">
<code className="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
variant="secondary"
size="sm"
className="gap-1.5 shadow-xs"
aria-label={copied ? 'Copied code' : 'Copy discount code'}
onClick={copyCoupon}
>
{copied ? <Check className="text-success size-3.5" /> : <Copy className="size-3.5" />}
<span>{copied ? 'Copied!' : 'Copy Code'}</span>
</Button>
</div>
<p className="text-muted-foreground text-xs">
Valid for the next <span className="font-mono font-medium">{formattedTime}</span> at checkout.
</p>
</div>
<div className="space-y-2 pt-2">
<Button
className="h-11 w-full justify-center gap-2 text-sm font-semibold shadow-xs"
onClick={handleDismiss}
>
Start Exploring Components
<ArrowRight className="size-4" />
</Button>
<button
type="button"
className="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"
onClick={handleReset}
>
Enter a different email
</button>
</div>
</div>
)}
</div>
</div>
)
}
Raw manifest:https://uipkge.dev/r/react/lead-capture-popup.json