
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 React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/lead-capture-popup.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/lead-capture-popup.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/lead-capture-popup.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/lead-capture-popup.jsonnpx shadcn-vue@latest add @uipkge/lead-capture-popupInstalls to:app/components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
headline | string | 'Unlock 20% Off Your First Order' | optional |
subtitle | string | 'Join 45 | optional |
discountCode | string | 'WELCOME20' | optional |
discountPercent | number | 20 | optional |
initialSeconds | number | 599 | optional |
socialProofRating | string | '4.9/5' | optional |
socialProofCount | string | '1 | optional |
initialSubmitted | boolean | false | optional |
class | HTMLAttributes['class'] | — | optional |
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ArrowRight, Check, CheckCircle2, Clock, Copy, Gift, RotateCcw, Star, X } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
interface Props {
headline?: string
subtitle?: string
discountCode?: string
discountPercent?: number
initialSeconds?: number
socialProofRating?: string
socialProofCount?: string
initialSubmitted?: boolean
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
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,
})
const emit = defineEmits<{
(e: 'submit', payload: { email: string; discountCode: string }): void
(e: 'dismiss'): void
(e: 'copy', code: string): void
(e: 'reset'): void
}>()
const email = ref('')
const isSubmitted = ref(props.initialSubmitted)
const isDismissed = ref(false)
const copied = ref(false)
const timeLeft = ref(props.initialSeconds)
let timer: ReturnType<typeof setInterval> | null = null
const formattedTime = computed(() => {
const mins = Math.floor(timeLeft.value / 60)
const secs = timeLeft.value % 60
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
})
onMounted(() => {
timer = setInterval(() => {
if (timeLeft.value > 0) {
timeLeft.value--
} else if (timer) {
clearInterval(timer)
}
}, 1000)
})
onUnmounted(() => {
if (timer) clearInterval(timer)
})
function handleSubmit() {
if (!email.value || !email.value.includes('@')) return
isSubmitted.value = true
emit('submit', { email: email.value, discountCode: props.discountCode })
}
function handleDismiss() {
isDismissed.value = true
emit('dismiss')
}
function handleReset() {
isSubmitted.value = false
isDismissed.value = false
email.value = ''
copied.value = false
timeLeft.value = props.initialSeconds
emit('reset')
}
function copyCoupon() {
navigator.clipboard?.writeText(props.discountCode)
copied.value = true
emit('copy', props.discountCode)
setTimeout(() => {
copied.value = false
}, 2000)
}
defineExpose({
reset: handleReset,
open: () => {
isDismissed.value = false
},
})
</script>
<template>
<div v-if="!isDismissed" data-slot="lead-capture-popup" :class="cn('w-full', props.class)">
<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"
>
<X class="size-4" />
</button>
<!-- FORM STATE -->
<div v-if="!isSubmitted" class="relative z-10 space-y-6">
<!-- Top Badge -->
<div class="flex items-center">
<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"
>
<Gift class="text-primary size-3.5" />
Limited Time Welcome Gift
</Badge>
</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">
<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 emerald 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">
<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">
<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">
<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.prevent="handleSubmit">
<Input
v-model="email"
type="email"
placeholder="[email protected]"
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 type="submit" size="lg" class="h-11 w-full justify-center gap-2 text-sm font-semibold shadow-xs">
Claim My {{ discountPercent }}% Discount
<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">
<Star class="size-3.5 fill-current" />
<Star class="size-3.5 fill-current" />
<Star class="size-3.5 fill-current" />
<Star class="size-3.5 fill-current" />
<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>
<!-- SUCCESS STATE -->
<div v-else 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">
<CheckCircle2 class="size-6" />
</div>
<div class="space-y-1.5">
<Badge variant="outline" class="border-success/30 bg-success/10 text-success font-medium">
{{ discountPercent }}% Discount Unlocked
</Badge>
<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 || '[email protected]' }}</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
variant="secondary"
size="sm"
class="gap-1.5 shadow-xs"
:aria-label="copied ? 'Copied code' : 'Copy discount code'"
@click="copyCoupon"
>
<Check v-if="copied" class="text-success size-3.5" />
<Copy v-else 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 class="h-11 w-full justify-center gap-2 text-sm font-semibold shadow-xs" @click="handleDismiss">
Start Exploring Components
<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>
<!-- Reopen trigger fallback when dismissed -->
<div
v-else
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 variant="outline" size="sm" class="mt-3 gap-2" @click="handleReset">
<RotateCcw class="text-primary size-3.5" />
Reopen Welcome Popup
</Button>
</div>
</template>
Raw manifest:https://uipkge.dev/r/vue/lead-capture-popup.json