7ovr
blocksaas-landing-2source: ok · hit
SaaS Landing 2
Product-tour SaaS landing page template: hero section with announcement pill, browser-framed gallery, tabbed features, how it works stepper, integrations, comparison, stats, testimonial, pricing, and FAQ.
npx shadcn add "https://7ovr.com/r/saas-landing-2.json"free
Where it goes
heropricingtestimonialfaqstatsonboardinggallerybannerfeature-grid
Interaction
animated-counter
Use cases
saas-landing
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@base-ui/react
Registry dependencies
accordionavatarbadgebuttoncarddialogfieldinputlabelseparatorsheetskeletonsonnertabstextareautils
Files15 · source cached 10/8/2026, 2:11:18 PM
registry/templates/saas-landing-2/install-page.tsx
import { Toaster } from "@/components/ui/sonner"
import Comparison from "@/components/saas-landing-2/comparison"
import Cta from "@/components/saas-landing-2/cta"
import Faqs from "@/components/saas-landing-2/faqs"
import Features from "@/components/saas-landing-2/features"
import Footer from "@/components/saas-landing-2/footer"
import Gallery from "@/components/saas-landing-2/gallery"
import Header from "@/components/saas-landing-2/header"
import Hero from "@/components/saas-landing-2/hero"
import HowItWorks from "@/components/saas-landing-2/how-it-works"
import Integrations from "@/components/saas-landing-2/integrations"
import Pricing from "@/components/saas-landing-2/pricing"
import Stats from "@/components/saas-landing-2/stats"
import Testimonials from "@/components/saas-landing-2/testimonials"
const pool = (size: string, color: string, mid: string, out: string) =>
`radial-gradient(${size}, ${color}, ${mid} 38%, ${out} 78%)`
const wash = (base: string, a: number, b: number) => {
const at = (v: number) => `rgba(${base},${v})`
return [
pool("38% 13% at 84% 9%", at(a), at(a / 2), at(0)),
pool("42% 13% at 12% 28%", at(b), at(b / 2), at(0)),
pool("40% 12% at 70% 47%", at(a), at(a / 2), at(0)),
pool("42% 12% at 14% 66%", at(b), at(b / 2), at(0)),
pool("40% 13% at 74% 87%", at(a), at(a / 2), at(0)),
].join(",")
}
const WASH_LIGHT = wash("0,0,0", 0.04, 0.034)
const WASH_DARK = wash("255,255,255", 0.06, 0.051)
export default function SaasLanding2Template() {
return (
<div className="relative isolate flex min-h-svh w-full flex-col bg-background text-foreground">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[720px] bg-[radial-gradient(65%_60%_at_50%_-8%,rgba(0,0,0,0.06),transparent_72%)] dark:bg-[radial-gradient(65%_60%_at_50%_-8%,rgba(255,255,255,0.13),transparent_72%)]"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 dark:hidden"
style={{ backgroundImage: WASH_LIGHT }}
/>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 hidden dark:block"
style={{ backgroundImage: WASH_DARK }}
/>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 bottom-0 -z-10 h-[560px] bg-[radial-gradient(55%_50%_at_50%_100%,rgba(0,0,0,0.05),transparent_70%)] dark:bg-[radial-gradient(55%_50%_at_50%_100%,rgba(255,255,255,0.08),transparent_70%)]"
/>
<Toaster />
<Header />
<main className="flex w-full flex-col">
<Hero />
<Gallery />
<Features />
<HowItWorks />
<Integrations />
<Comparison />
<Stats />
<Testimonials />
<Pricing />
<Faqs />
<Cta />
</main>
<div className="border-t border-border/50">
<Footer />
</div>
</div>
)
}
registry/templates/saas-landing-2/install-route.tsx
export { default } from "@/components/saas-landing-2/page"
registry/templates/saas-landing-2/components/comparison.tsx
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"
const acmePoints = [
"Onboarding completed in under 10 minutes",
"Real-time sync across all devices and team members",
"Granular role-based access control included",
"99.99 % uptime SLA with status page",
"Dedicated Slack channel for Pro customers",
"SOC 2 Type II certified infrastructure",
"One-click data export in any format",
"No per-seat fees, unlimited collaborators",
]
const othersPoints = [
"Complex setup requires engineering resources",
"Sync delays of up to 15 minutes on free tiers",
"Permissions locked behind enterprise plan",
"SLA only available on custom contracts",
"Email-only support with 48 h response time",
"Compliance docs behind sales call",
"Data export limited to CSV on lower plans",
"Per-seat pricing adds up fast at scale",
]
function CheckRow({ text }: { text: string }) {
return (
<li className="flex items-start gap-3">
<span className="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-md bg-primary">
<IconPlaceholder
lucide="Check"
tabler="IconCheck"
hugeicons="Tick02Icon"
phosphor="Check"
remixicon="RiCheckLine"
className="size-3 text-primary-foreground"
aria-hidden
/>
</span>
<span className="text-sm text-foreground">{text}</span>
</li>
)
}
function CrossRow({ text }: { text: string }) {
return (
<li className="flex items-start gap-3">
<span className="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-md bg-muted">
<IconPlaceholder
lucide="X"
tabler="IconX"
hugeicons="Cancel01Icon"
phosphor="X"
remixicon="RiCloseLine"
className="size-3 text-muted-foreground"
aria-hidden
/>
</span>
<span className="text-sm text-muted-foreground">{text}</span>
</li>
)
}
export default function Comparison() {
return (
<section className="flex w-full items-center justify-center px-6 py-16 sm:py-24">
<div className="mx-auto w-full max-w-4xl">
<div className="mb-10 text-center">
<span className="mb-4 block text-xs font-semibold tracking-widest text-muted-foreground uppercase">
Why Acme
</span>
<h2 className="font-heading text-3xl font-bold tracking-tight text-balance sm:text-4xl">
Built differently, on purpose
</h2>
<p className="mt-4 text-sm text-muted-foreground">
We obsessed over the details others skip. Here is what that means
for your team every single day.
</p>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<Card className="border-primary/30 ring-primary/20">
<CardHeader>
<div className="flex items-center gap-2">
<CardTitle className="text-base font-semibold">Acme</CardTitle>
<Badge variant="default">Recommended</Badge>
</div>
<CardDescription className="sm:min-h-10">
Everything your team needs, without the enterprise runaround.
</CardDescription>
</CardHeader>
<Separator />
<CardContent className="pt-4">
<ul className="flex flex-col gap-3">
{acmePoints.map((point) => (
<CheckRow key={point} text={point} />
))}
</ul>
</CardContent>
<CardFooter className="border-t">
<Button
nativeButton={false}
className="w-full"
render={<a href="#" />}
>
Start For Free
<IconPlaceholder
lucide="ArrowRight"
tabler="IconArrowRight"
hugeicons="ArrowRightIcon"
phosphor="ArrowRight"
remixicon="RiArrowRightLine"
data-icon="inline-end"
/>
</Button>
</CardFooter>
</Card>
<Card className="bg-muted/30">
<CardHeader>
<CardTitle className="text-base font-semibold text-muted-foreground">
The others
</CardTitle>
<CardDescription className="sm:min-h-10">
Common friction points teams encounter with legacy platforms.
</CardDescription>
</CardHeader>
<Separator />
<CardContent className="pt-4">
<ul className="flex flex-col gap-3">
{othersPoints.map((point) => (
<CrossRow key={point} text={point} />
))}
</ul>
</CardContent>
<CardFooter className="border-t">
<Button
variant="secondary"
nativeButton={false}
className="w-full"
render={<a href="#" />}
>
See Why Teams Switch
<IconPlaceholder
lucide="ArrowRight"
tabler="IconArrowRight"
hugeicons="ArrowRightIcon"
phosphor="ArrowRight"
remixicon="RiArrowRightLine"
data-icon="inline-end"
/>
</Button>
</CardFooter>
</Card>
</div>
</div>
</section>
)
}
registry/templates/saas-landing-2/components/cta.tsx
"use client"
import { useState } from "react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"
export default function Cta() {
const [email, setEmail] = useState("")
const [submitted, setSubmitted] = useState(false)
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
if (!email) return
setSubmitted(true)
}
return (
<section className="flex w-full items-center justify-center px-6 py-16 sm:py-24">
<div className="mx-auto w-full max-w-2xl rounded-lg border border-border bg-muted/30 px-6 py-12 text-center sm:px-12 sm:py-16">
<span className="mb-4 block text-xs font-semibold tracking-widest text-muted-foreground uppercase">
Get Started
</span>
<h2 className="font-heading text-3xl font-bold tracking-tight text-balance sm:text-4xl">
Start building with Acme today
</h2>
<p className="mx-auto mt-4 max-w-lg text-base text-pretty text-muted-foreground">
Join thousands of teams shipping faster. Get a 14-day free trial, no
credit card required.
</p>
<form
onSubmit={handleSubmit}
className="mx-auto mt-8 flex w-full max-w-md flex-col gap-3 sm:flex-row"
>
<div className="flex-1 text-left">
<Label htmlFor="cta-email" className="sr-only">
Email address
</Label>
<Input
id="cta-email"
type="email"
required
placeholder="you@acme.com"
value={email}
onChange={(event) => setEmail(event.target.value)}
className="h-11 w-full"
/>
</div>
<Button type="submit" className="h-11 shrink-0">
Start Free Trial
<IconPlaceholder
lucide="ArrowRight"
tabler="IconArrowRight"
hugeicons="ArrowRightIcon"
phosphor="ArrowRight"
remixicon="RiArrowRightLine"
data-icon="inline-end"
aria-hidden="true"
/>
</Button>
</form>
<p
className={cn(
"mt-3 text-sm text-primary transition-opacity",
submitted ? "opacity-100" : "opacity-0"
)}
aria-live="polite"
>
Thanks! Check your inbox to confirm your trial.
</p>
<p className="mt-4 flex items-center justify-center gap-1.5 text-xs text-muted-foreground">
<IconPlaceholder
lucide="ShieldCheck"
tabler="IconShieldCheck"
hugeicons="Shield01Icon"
phosphor="ShieldCheck"
remixicon="RiShieldCheckLine"
className="size-3.5"
data-icon="inline-start"
aria-hidden="true"
/>
We respect your privacy. No spam, unsubscribe anytime.
</p>
</div>
</section>
)
}
registry/templates/saas-landing-2/components/faqs.tsx
"use client"
import { useState } from "react"
import { toast } from "sonner"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"
const faqs = [
{
q: "How do I install a block from the registry?",
a: "Copy the install command from any block page and run it with the shadcn CLI. The block, its dependencies, and required primitives are added straight to your project.",
},
{
q: "Do the blocks work with my existing theme?",
a: "Yes. Every block is built on shadcn theme tokens, so it inherits your colors, radius, and typography automatically and renders correctly in both light and dark mode.",
},
{
q: "Can I customize a block after installing it?",
a: "Absolutely. Blocks are copied directly into your codebase as plain, editable source, so you own the code and can change markup, styles, and logic however you like.",
},
{
q: "What is the difference between free and Pro blocks?",
a: "Free blocks are open and installable by anyone. Pro blocks unlock advanced layouts, dashboards, and full-page templates, served through your authenticated Acme account.",
},
{
q: "Will updates to a block overwrite my changes?",
a: "No. Because blocks live in your repository, reinstalling a newer version is opt-in. You decide when to pull updates and how to merge them with your edits.",
},
{
q: "Which frameworks are supported?",
a: "Blocks target React 19 and Next.js with the App Router, styled using Tailwind CSS. Most are framework-agnostic enough to drop into any modern React setup.",
},
]
export default function Faqs() {
const [open, setOpen] = useState(false)
return (
<section className="flex w-full items-center justify-center px-6 py-16 sm:py-24">
<div className="mx-auto grid w-full max-w-5xl gap-12 md:grid-cols-[1.4fr_1fr] md:gap-16">
<div>
<span className="mb-4 block text-xs font-semibold tracking-widest text-muted-foreground uppercase">
FAQ
</span>
<h2 className="font-heading text-3xl font-bold tracking-tight text-balance sm:text-4xl">
Frequently asked questions
</h2>
<p className="mt-4 text-muted-foreground">
Everything you need to know about installing and using Acme blocks.
</p>
<Accordion defaultValue={[faqs[0].q]} className="mt-8">
{faqs.map(({ q, a }) => (
<AccordionItem key={q} value={q}>
<AccordionTrigger className="py-4 text-base font-medium">
{q}
</AccordionTrigger>
<AccordionContent className="pb-4 text-base text-muted-foreground">
{a}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
<div className="md:pt-16">
<Card className="md:sticky md:top-8">
<CardHeader>
<span className="flex size-10 items-center justify-center rounded-full bg-primary/10 text-primary">
<IconPlaceholder
lucide="Headset"
tabler="IconHeadset"
hugeicons="CustomerSupportIcon"
phosphor="Headset"
remixicon="RiCustomerService2Line"
className="size-5"
/>
</span>
<CardTitle className="mt-4 text-xl">
Still have questions?
</CardTitle>
<CardDescription className="text-base">
Can't find the answer you're looking for? Our support
team usually replies within a few hours.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button className="w-full">
<IconPlaceholder
lucide="Mail"
tabler="IconMail"
hugeicons="MailIcon"
phosphor="Envelope"
remixicon="RiMailLine"
data-icon="inline-start"
/>
Contact Support
<IconPlaceholder
lucide="ArrowRight"
tabler="IconArrowRight"
hugeicons="ArrowRightIcon"
phosphor="ArrowRight"
remixicon="RiArrowRightLine"
data-icon="inline-end"
/>
</Button>
}
/>
<DialogContent>
<form
onSubmit={(event) => {
event.preventDefault()
setOpen(false)
toast.success(
"Message sent. Our support team will reply shortly."
)
}}
>
<DialogHeader>
<DialogTitle>Contact support</DialogTitle>
<DialogDescription>
Send us a message and we'll get back to you within
a few hours.
</DialogDescription>
</DialogHeader>
<FieldGroup className="my-4">
<Field>
<FieldLabel htmlFor="support-email">
Work email
</FieldLabel>
<Input
id="support-email"
type="email"
placeholder="jane@company.com"
required
/>
</Field>
<Field>
<FieldLabel htmlFor="support-message">
How can we help?
</FieldLabel>
<Textarea
id="support-message"
rows={4}
className="resize-none"
placeholder="Describe your question…"
required
/>
</Field>
</FieldGroup>
<DialogFooter showCloseButton>
<Button type="submit">
<IconPlaceholder
lucide="Mail"
tabler="IconMail"
hugeicons="MailIcon"
phosphor="Envelope"
remixicon="RiMailLine"
data-icon="inline-start"
/>
Send Message
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Button
variant="outline"
render={<a href="#" />}
nativeButton={false}
className="w-full"
>
Browse Documentation
</Button>
</CardContent>
</Card>
</div>
</div>
</section>
)
}
registry/templates/saas-landing-2/components/features.tsx
import { Card } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { cn } from "@/lib/utils"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"
type IconProps = { className?: string; size?: number | string }
type Feature = {
icon: React.ComponentType<{ className?: string }>
title: string
description: string
}
type FeatureTab = {
value: string
label: string
features: Feature[]
}
const TABS: FeatureTab[] = [
{
value: "collaboration",
label: "Collaboration",
features: [
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="Group"
tabler="IconUsers"
hugeicons="GroupIcon"
phosphor="Users"
remixicon="RiGroupLine"
{...p}
/>
),
title: "Shared workspaces",
description:
"Bring every team into one workspace with granular roles and instant invites.",
},
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="MessageCircle"
tabler="IconMessageCircle"
hugeicons="Message01Icon"
phosphor="ChatCircle"
remixicon="RiChat3Line"
{...p}
/>
),
title: "Inline comments",
description:
"Discuss changes in context with threaded comments, mentions, and reactions.",
},
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="History"
tabler="IconHistory"
hugeicons="HistoryIcon"
phosphor="ClockCounterClockwise"
remixicon="RiHistoryLine"
{...p}
/>
),
title: "Version history",
description:
"Track every edit and restore any previous state with a single click.",
},
],
},
{
value: "automation",
label: "Automation",
features: [
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="Flashlight"
tabler="IconBulb"
hugeicons="FlashlightIcon"
phosphor="Flashlight"
remixicon="RiFlashlightLine"
{...p}
/>
),
title: "Visual workflows",
description:
"Chain triggers and actions on a drag-and-drop canvas, no code required.",
},
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="Bot"
tabler="IconRobot"
hugeicons="RobotIcon"
phosphor="Robot"
remixicon="RiRobot2Line"
{...p}
/>
),
title: "Smart agents",
description:
"Let Acme agents triage requests, draft replies, and route work for you.",
},
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="Timer"
tabler="IconClockBolt"
hugeicons="TimerIcon"
phosphor="Timer"
remixicon="RiTimerFlashLine"
{...p}
/>
),
title: "Scheduled runs",
description:
"Queue recurring jobs down to the minute with built-in retries and alerts.",
},
],
},
{
value: "security",
label: "Security",
features: [
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="ShieldCheck"
tabler="IconShieldCheck"
hugeicons="Shield01Icon"
phosphor="ShieldCheck"
remixicon="RiShieldCheckLine"
{...p}
/>
),
title: "SOC 2 Type II",
description:
"Independently audited controls keep your data compliant and protected.",
},
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="Key"
tabler="IconKey"
hugeicons="KeyIcon"
phosphor="Key"
remixicon="RiKeyLine"
{...p}
/>
),
title: "SSO & SCIM",
description:
"Provision users through SAML, OIDC, and automated directory sync.",
},
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="Fingerprint"
tabler="IconFingerprint"
hugeicons="FingerPrintIcon"
phosphor="Fingerprint"
remixicon="RiFingerprintLine"
{...p}
/>
),
title: "Audit logging",
description:
"Every action is timestamped, immutable, and exportable to your SIEM.",
},
],
},
]
export default function Features() {
return (
<section className="flex w-full items-center justify-center px-6 py-16 sm:py-24">
<div className="mx-auto w-full max-w-5xl">
<div className="flex flex-col items-center text-center">
<span className="mb-4 block text-xs font-semibold tracking-widest text-muted-foreground uppercase">
Built For Teams
</span>
<h2 className="font-heading text-3xl font-bold tracking-tight text-balance sm:text-4xl">
Everything you need to ship faster
</h2>
<p className="mt-4 max-w-xl text-pretty text-muted-foreground">
Acme brings collaboration, automation, and enterprise-grade security
together in one connected platform.
</p>
</div>
<Tabs
defaultValue="collaboration"
className="mt-10 w-full items-center"
>
<TabsList className="h-auto flex-wrap gap-1 rounded-lg p-1">
{TABS.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className="rounded-md px-4 py-1.5 text-sm"
>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{TABS.map((tab) => (
<TabsContent key={tab.value} value={tab.value} className="mt-8">
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3">
{tab.features.map((feature) => {
const Icon = feature.icon
return (
<Card
key={feature.title}
className="group h-full gap-4 p-6 transition-colors hover:border-primary/40"
>
<div
className={cn(
"flex size-11 items-center justify-center rounded-md",
"bg-primary/10 text-primary transition-colors",
"group-hover:bg-primary group-hover:text-primary-foreground"
)}
>
<Icon className="size-5" />
</div>
<div className="space-y-1.5">
<h3 className="font-heading text-base font-medium">
{feature.title}
</h3>
<p className="text-sm/relaxed text-muted-foreground">
{feature.description}
</p>
</div>
<div className="mt-auto flex items-center gap-2 text-xs font-medium text-muted-foreground">
<IconPlaceholder
lucide="GitBranch"
tabler="IconGitBranch"
hugeicons="GitBranchIcon"
phosphor="GitBranch"
remixicon="RiGitBranchLine"
className="size-3.5"
/>
Included on every plan
</div>
</Card>
)
})}
</div>
</TabsContent>
))}
</Tabs>
</div>
</section>
)
}
registry/templates/saas-landing-2/components/footer.tsx
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
const columns = [
{ title: "Product", links: ["Blocks", "Templates", "Pricing", "Changelog"] },
{ title: "Resources", links: ["Docs", "Guides", "Support", "API"] },
{ title: "Company", links: ["About", "Blog", "Careers", "Contact"] },
]
const socials = [
{ label: "GitHub", icon: GithubMark },
{ label: "X", icon: XMark },
{ label: "Discord", icon: DiscordMark },
]
export default function Footer() {
return (
<section className="flex w-full flex-col">
<footer className="w-full px-4 py-10 sm:px-6">
<div className="grid gap-8 md:grid-cols-2">
<div className="max-w-sm">
<a href="#" className="flex items-center gap-2">
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
className="size-6 shrink-0 text-primary"
>
<rect
x="3"
y="3"
width="8"
height="8"
transform="rotate(-6 7 7)"
/>
<rect
x="3"
y="13"
width="8"
height="8"
transform="rotate(5 7 17)"
/>
<rect
x="13"
y="13"
width="8"
height="8"
transform="rotate(-4 17 17)"
/>
<rect
x="13"
y="3"
width="8"
height="8"
transform="rotate(15 17 7)"
/>
</svg>
<span className="text-lg font-bold tracking-tight">Acme</span>
</a>
<p className="mt-3 text-sm text-muted-foreground">
Everything your team needs to build, ship, and scale.
</p>
</div>
<div className="grid grid-cols-2 gap-8 sm:grid-cols-3">
{columns.map((col) => (
<div key={col.title}>
<h3 className="font-heading text-sm font-bold tracking-tight">
{col.title}
</h3>
<ul className="mt-3 flex flex-col gap-2">
{col.links.map((link) => (
<li key={link}>
<a
href="#"
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
>
{link}
</a>
</li>
))}
</ul>
</div>
))}
</div>
</div>
<Separator className="mt-10" />
<div className="flex flex-col items-start gap-4 pt-6 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm text-muted-foreground">
© 2026 Acme. All rights reserved.
</p>
<div className="flex items-center gap-2">
{socials.map((social) => (
<Button
key={social.label}
variant="outline"
size="icon-sm"
aria-label={social.label}
render={<a href="#" />}
nativeButton={false}
className="rounded-lg text-muted-foreground hover:text-foreground"
>
<social.icon className="size-4" aria-hidden="true" />
</Button>
))}
</div>
</div>
</footer>
</section>
)
}
type MarkProps = React.ComponentProps<"svg"> & { size?: number | string }
function DiscordMark({ size = 24, ...props }: MarkProps) {
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M19.3034 5.33716C17.9344 4.71103 16.4805 4.2547 14.9629 4C14.7719 4.32899 14.5596 4.77471 14.411 5.12492C12.7969 4.89144 11.1944 4.89144 9.60255 5.12492C9.45397 4.77471 9.2311 4.32899 9.05068 4C7.52251 4.2547 6.06861 4.71103 4.70915 5.33716C1.96053 9.39111 1.21766 13.3495 1.5891 17.2549C3.41443 18.5815 5.17612 19.388 6.90701 19.9187C7.33151 19.3456 7.71356 18.73 8.04255 18.0827C7.41641 17.8492 6.82211 17.5627 6.24904 17.2231C6.39762 17.117 6.5462 17.0003 6.68416 16.8835C10.1438 18.4648 13.8911 18.4648 17.3082 16.8835C17.4568 17.0003 17.5948 17.117 17.7434 17.2231C17.1703 17.5627 16.576 17.8492 15.9499 18.0827C16.2789 18.73 16.6609 19.3456 17.0854 19.9187C18.8152 19.388 20.5875 18.5815 22.4033 17.2549C22.8596 12.7341 21.6806 8.80747 19.3034 5.33716ZM8.5201 14.8459C7.48007 14.8459 6.63107 13.9014 6.63107 12.7447C6.63107 11.5879 7.45884 10.6434 8.5201 10.6434C9.57071 10.6434 10.4303 11.5879 10.4091 12.7447C10.4091 13.9014 9.57071 14.8459 8.5201 14.8459ZM15.4936 14.8459C14.4535 14.8459 13.6034 13.9014 13.6034 12.7447C13.6034 11.5879 14.4323 10.6434 15.4936 10.6434C16.5442 10.6434 17.4038 11.5879 17.3825 12.7447C17.3825 13.9014 16.5548 14.8459 15.4936 14.8459Z" />
</svg>
)
}
function GithubMark({ size = 24, ...props }: MarkProps) {
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z" />
</svg>
)
}
function XMark({ size = 24, ...props }: MarkProps) {
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M17.6874 3.0625L12.6907 8.77425L8.37045 3.0625H2.11328L9.58961 12.8387L2.50378 20.9375H5.53795L11.0068 14.6886L15.7863 20.9375H21.8885L14.095 10.6342L20.7198 3.0625H17.6874ZM16.6232 19.1225L5.65436 4.78217H7.45745L18.3034 19.1225H16.6232Z" />
</svg>
)
}
registry/templates/saas-landing-2/components/gallery.tsx
"use client"
import { useState } from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"
const screens = [
{
id: 1,
label: "Acme Dashboard: Overview",
url: "app.acme.io/dashboard",
version: "v2.4",
description:
"A unified command center for monitoring pipeline health, active deployments, and real-time system metrics across all environments.",
src: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=1200&q=80",
alt: "Acme dashboard overview showing pipeline metrics",
},
{
id: 2,
label: "Analytics",
url: "app.acme.io/analytics",
description:
"Deep-dive into usage trends, funnel conversion, and retention cohorts.",
src: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&q=75",
alt: "Analytics charts and metrics",
},
{
id: 3,
label: "Deployments",
url: "app.acme.io/deployments",
description:
"Track every release: status, rollback controls, and canary traffic splits.",
src: "https://images.unsplash.com/photo-1667372393119-3d4c48d07fc9?w=800&q=75",
alt: "Deployment pipeline and release status",
},
{
id: 4,
label: "Team Settings",
url: "app.acme.io/team",
description:
"Manage members, roles, SSO configuration, and access policies.",
src: "https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&q=75",
alt: "Team settings and member management",
},
{
id: 5,
label: "Audit Logs",
url: "app.acme.io/audit",
description:
"Immutable event history with filtering, export, and alert rules.",
src: "https://images.unsplash.com/photo-1555949963-ff9fe0c870eb?w=800&q=75",
alt: "Audit log event timeline",
},
{
id: 6,
label: "API Keys",
url: "app.acme.io/api-keys",
description:
"Generate, rotate, and scope API credentials with fine-grained permissions.",
src: "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&q=75",
alt: "API key management interface",
},
]
export default function Gallery() {
const [activeId, setActiveId] = useState(screens[0].id)
const active = screens.find((screen) => screen.id === activeId) ?? screens[0]
const thumbnails = screens.slice(1)
return (
<section className="flex w-full items-center justify-center px-6 py-16 sm:py-24">
<div className="mx-auto flex w-full max-w-5xl flex-col gap-8">
<div className="flex flex-col gap-4">
<span className="block text-xs font-semibold tracking-widest text-muted-foreground uppercase">
Product Screenshots
</span>
<h2 className="font-heading text-3xl font-bold tracking-tight text-balance sm:text-4xl">
See Acme in action
</h2>
</div>
<div className="rounded-xl border border-border bg-card shadow-sm">
<div className="flex items-center gap-2.5 rounded-t-xl border-b border-border bg-muted px-4 py-2.5">
<div className="flex items-center gap-1.5">
<span className="size-2.5 border border-border bg-background" />
<span className="size-2.5 border border-border bg-background" />
<span className="size-2.5 border border-border bg-background" />
</div>
<div className="mx-2 flex flex-1 items-center gap-2 rounded-md border border-border bg-background px-3 py-1">
<span className="size-1.5 bg-muted-foreground/30" />
<span className="flex-1 truncate text-xs text-muted-foreground">
{active.url}
</span>
</div>
<IconPlaceholder
lucide="ZoomIn"
tabler="IconZoomIn"
hugeicons="SearchAddIcon"
phosphor="MagnifyingGlassPlus"
remixicon="RiZoomInLine"
className="size-4 shrink-0 text-muted-foreground/40"
aria-hidden="true"
/>
</div>
<Dialog>
<DialogTrigger
render={
<button
type="button"
aria-label={`Enlarge ${active.label}`}
className="group relative block aspect-video w-full cursor-zoom-in overflow-hidden rounded-b-xl bg-muted text-left focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
/>
}
>
<img
key={active.id}
src={active.src}
alt={active.alt}
className="size-full object-cover grayscale transition-[transform,filter] duration-300 group-hover:scale-[1.02] group-hover:grayscale-0"
/>
<span className="pointer-events-none absolute right-3 bottom-3 flex items-center gap-1.5 rounded-md bg-foreground/70 px-2 py-1 text-[10px] font-medium tracking-wide text-background opacity-0 backdrop-blur-sm transition-opacity duration-200 group-hover:opacity-100">
<IconPlaceholder
lucide="ZoomIn"
tabler="IconZoomIn"
hugeicons="SearchAddIcon"
phosphor="MagnifyingGlassPlus"
remixicon="RiZoomInLine"
className="size-3.5"
aria-hidden="true"
/>
Enlarge
</span>
</DialogTrigger>
<DialogContent showCloseButton className="gap-0 p-0 sm:max-w-3xl">
<DialogTitle className="sr-only">{active.label}</DialogTitle>
<DialogDescription className="sr-only">
{active.description}
</DialogDescription>
<div className="aspect-video w-full overflow-hidden rounded-t-xl bg-muted">
<img
src={active.src}
alt={active.alt}
className="size-full object-contain"
/>
</div>
<div className="flex items-center justify-between gap-3 border-t border-border px-4 py-3">
<span className="text-sm font-semibold tracking-tight text-foreground">
{active.label}
</span>
<span className="truncate text-xs text-muted-foreground">
{active.url}
</span>
</div>
</DialogContent>
</Dialog>
</div>
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<span className="text-base font-semibold tracking-tight">
{active.label}
</span>
{active.version && (
<Badge
variant="secondary"
className="text-[10px] font-medium tracking-wide"
>
{active.version}
</Badge>
)}
</div>
<p className="max-w-lg text-sm leading-relaxed text-muted-foreground">
{active.description}
</p>
</div>
<Button
render={<a href="#" />}
nativeButton={false}
variant="outline"
className="shrink-0 gap-2 text-xs font-medium"
>
<IconPlaceholder
lucide="ExternalLink"
tabler="IconExternalLink"
hugeicons="ExternalLinkIcon"
phosphor="ArrowSquareOut"
remixicon="RiExternalLinkLine"
className="size-3.5"
aria-hidden="true"
/>
View Live Demo
</Button>
</div>
<div className="border-t border-border" />
<div className="flex flex-col gap-3">
<span className="text-xs font-semibold tracking-widest text-muted-foreground uppercase">
More Screens
</span>
<div className="grid grid-cols-3 gap-3 sm:grid-cols-5">
{thumbnails.map((thumb) => {
const isActive = thumb.id === activeId
return (
<button
key={thumb.id}
type="button"
onClick={() => setActiveId(thumb.id)}
className="group flex cursor-pointer flex-col gap-1.5 text-left focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
>
<div
className={[
"relative aspect-video w-full overflow-hidden rounded-md border transition-all duration-200",
isActive
? "border-foreground/60 ring-1 ring-foreground/20"
: "border-border group-hover:border-foreground/30",
].join(" ")}
>
<img
src={thumb.src}
alt={thumb.alt}
className="size-full object-cover grayscale transition-[transform,filter] duration-300 group-hover:scale-[1.04] group-hover:grayscale-0"
/>
{!isActive && (
<div className="absolute inset-0 bg-foreground/0 transition-colors duration-200 group-hover:bg-foreground/5" />
)}
</div>
<span
className={[
"truncate text-xs font-medium transition-colors",
isActive
? "text-foreground"
: "text-muted-foreground group-hover:text-foreground/70",
].join(" ")}
>
{thumb.label}
</span>
</button>
)
})}
</div>
</div>
</div>
</section>
)
}
registry/templates/saas-landing-2/components/header.tsx
"use client"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetClose,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"
const navLinks = [
{ label: "Products", href: "#" },
{ label: "Solutions", href: "#" },
{ label: "Pricing", href: "#" },
{ label: "Docs", href: "#" },
{ label: "Blog", href: "#" },
]
export default function Header() {
return (
<header className="sticky top-0 z-30 flex h-16 w-full items-center border-b border-border bg-background/80 px-6 backdrop-blur">
<a href="#" className="flex shrink-0 items-center gap-2.5">
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
className="size-6 shrink-0 text-primary"
>
<rect x="3" y="3" width="8" height="8" transform="rotate(-6 7 7)" />
<rect x="3" y="13" width="8" height="8" transform="rotate(5 7 17)" />
<rect
x="13"
y="13"
width="8"
height="8"
transform="rotate(-4 17 17)"
/>
<rect x="13" y="3" width="8" height="8" transform="rotate(15 17 7)" />
</svg>
<span className="text-base font-bold tracking-tight">Acme</span>
</a>
<nav className="absolute left-1/2 hidden -translate-x-1/2 items-center gap-1 md:flex">
{navLinks.map((link) => (
<Button
key={link.label}
render={<a href={link.href} />}
nativeButton={false}
variant="ghost"
className="text-muted-foreground hover:text-foreground"
>
{link.label}
</Button>
))}
</nav>
<div className="ml-auto flex shrink-0 items-center gap-3">
<Button
render={<a href="#" />}
nativeButton={false}
variant="ghost"
className="hidden text-muted-foreground hover:text-foreground sm:inline-flex"
>
Sign In
</Button>
<Separator orientation="vertical" className="hidden h-5 sm:block" />
<Button
render={<a href="#" />}
nativeButton={false}
className="hidden sm:inline-flex"
>
Start Free Trial
<IconPlaceholder
lucide="ArrowRight"
tabler="IconArrowRight"
hugeicons="ArrowRightIcon"
phosphor="ArrowRight"
remixicon="RiArrowRightLine"
data-icon="inline-end"
aria-hidden="true"
/>
</Button>
<Sheet>
<SheetTrigger
render={
<Button variant="outline" size="icon" className="md:hidden" />
}
aria-label="Open menu"
>
<IconPlaceholder
lucide="Menu"
tabler="IconMenu"
hugeicons="MenuIcon"
phosphor="List"
remixicon="RiMenuLine"
aria-hidden="true"
/>
</SheetTrigger>
<SheetContent side="right" className="w-full sm:max-w-xs">
<SheetHeader>
<SheetTitle className="flex items-center gap-2.5">
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
className="size-5 shrink-0 text-primary"
>
<rect
x="3"
y="3"
width="8"
height="8"
transform="rotate(-6 7 7)"
/>
<rect
x="3"
y="13"
width="8"
height="8"
transform="rotate(5 7 17)"
/>
<rect
x="13"
y="13"
width="8"
height="8"
transform="rotate(-4 17 17)"
/>
<rect
x="13"
y="3"
width="8"
height="8"
transform="rotate(15 17 7)"
/>
</svg>
Acme
</SheetTitle>
</SheetHeader>
<nav className="flex flex-col px-2">
{navLinks.map((link) => (
<SheetClose
key={link.label}
render={<a href={link.href} />}
nativeButton={false}
className="rounded-md px-2 py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
{link.label}
</SheetClose>
))}
</nav>
<SheetFooter>
<Button
render={<a href="#" />}
nativeButton={false}
variant="ghost"
className="w-full"
>
Sign In
</Button>
<Button
render={<a href="#" />}
nativeButton={false}
className="w-full"
>
Start Free Trial
<IconPlaceholder
lucide="ArrowRight"
tabler="IconArrowRight"
hugeicons="ArrowRightIcon"
phosphor="ArrowRight"
remixicon="RiArrowRightLine"
data-icon="inline-end"
aria-hidden="true"
/>
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</div>
</header>
)
}
registry/templates/saas-landing-2/components/hero.tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"
const avatars = [
{ src: "https://i.pravatar.cc/80?img=45", name: "Elena", initials: "E" },
{ src: "https://i.pravatar.cc/80?img=13", name: "Marcus", initials: "M" },
{ src: "https://i.pravatar.cc/80?img=32", name: "Priya", initials: "P" },
{ src: "https://i.pravatar.cc/80?img=52", name: "Tomás", initials: "T" },
{ src: "https://i.pravatar.cc/80?img=27", name: "Hannah", initials: "H" },
]
export default function Hero() {
return (
<section className="flex w-full items-center justify-center px-6 py-16 sm:py-24">
<div className="mx-auto flex max-w-2xl flex-col items-center text-center">
<a
href="#"
className="group inline-flex items-center gap-2 rounded-4xl border border-border bg-muted/40 py-1 pr-2 pl-2.5 text-xs font-medium text-foreground transition-colors hover:bg-muted"
>
<IconPlaceholder
lucide="Sparkles"
tabler="IconSparkles"
hugeicons="SparklesIcon"
phosphor="Sparkle"
remixicon="RiSparkling2Line"
className="size-3.5 text-foreground"
aria-hidden="true"
/>
<span>Introducing real-time collaboration</span>
<IconPlaceholder
lucide="ChevronRight"
tabler="IconChevronRight"
hugeicons="ArrowRight01Icon"
phosphor="CaretRight"
remixicon="RiArrowRightSLine"
className="size-3.5 transition-transform group-hover:translate-x-0.5"
aria-hidden="true"
/>
</a>
<h1 className="mt-6 font-heading text-4xl font-bold tracking-tight text-balance sm:text-5xl">
Ship your product faster than ever
</h1>
<p className="mt-5 max-w-xl text-base text-pretty text-muted-foreground">
The all-in-one platform that helps teams design, build, and launch
without the busywork. Free to start, no credit card required.
</p>
<div className="mt-8 flex flex-col items-center gap-3 sm:flex-row">
<Button
render={<a href="#" />}
nativeButton={false}
variant="outline"
size="lg"
>
Book a demo
</Button>
<Button render={<a href="#" />} nativeButton={false} size="lg">
Get started free
<IconPlaceholder
lucide="ChevronRight"
tabler="IconChevronRight"
hugeicons="ArrowRight01Icon"
phosphor="CaretRight"
remixicon="RiArrowRightSLine"
data-icon="inline-end"
aria-hidden="true"
/>
</Button>
</div>
<div className="mt-10 flex flex-col items-center gap-3 sm:flex-row sm:gap-4">
<div className="flex -space-x-2">
{avatars.map((avatar) => (
<Avatar
key={avatar.name}
className="size-8 border-2 border-background"
>
<AvatarImage
src={avatar.src}
alt={avatar.name}
className="grayscale"
/>
<AvatarFallback className="text-[10px]">
{avatar.initials}
</AvatarFallback>
</Avatar>
))}
</div>
<div className="flex flex-col items-center gap-0.5 sm:items-start">
<div
className="flex items-center gap-0.5"
role="img"
aria-label="5 out of 5"
>
{Array.from({ length: 5 }).map((_, index) => (
<IconPlaceholder
lucide="Star"
tabler="IconStar"
hugeicons="StarIcon"
phosphor="Star"
remixicon="RiStarFill"
key={index}
className="size-4 text-foreground"
aria-hidden="true"
/>
))}
</div>
<span className="text-xs text-muted-foreground">
Trusted by 2,000+ teams worldwide
</span>
</div>
</div>
</div>
</section>
)
}
registry/templates/saas-landing-2/components/how-it-works.tsx
"use client"
import { useState } from "react"
import { cn } from "@/lib/utils"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"
type IconProps = { className?: string; size?: number | string }
const steps = [
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="UserPlus"
tabler="IconUserPlus"
hugeicons="UserAddIcon"
phosphor="UserPlus"
remixicon="RiUserAddLine"
{...p}
/>
),
title: "Create your account",
copy: "Sign up in under two minutes, no card required to get started.",
},
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="Palette"
tabler="IconPalette"
hugeicons="PaintBoardIcon"
phosphor="Palette"
remixicon="RiPaletteLine"
{...p}
/>
),
title: "Customize your space",
copy: "Pick a template and tune Acme to match how your team already works.",
},
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="Users"
tabler="IconUsersGroup"
hugeicons="UserGroupIcon"
phosphor="UsersThree"
remixicon="RiTeamLine"
{...p}
/>
),
title: "Invite your team",
copy: "Send role-based invites in bulk and let colleagues join in one click.",
},
{
icon: (p: IconProps) => (
<IconPlaceholder
lucide="Rocket"
tabler="IconRocket"
hugeicons="RocketIcon"
phosphor="Rocket"
remixicon="RiRocketLine"
{...p}
/>
),
title: "Ship with confidence",
copy: "Run automated checks and deploy knowing Acme has your back.",
},
]
export default function HowItWorks() {
const [active, setActive] = useState(0)
const progress = ((active + 0.5) / steps.length) * 100
return (
<section className="flex w-full items-center justify-center px-6 py-16 sm:py-24">
<div className="mx-auto w-full max-w-4xl">
<div className="mb-12 text-center">
<span className="mb-4 block text-xs font-semibold tracking-widest text-muted-foreground uppercase">
How It Works
</span>
<h2 className="font-heading text-3xl font-bold tracking-tight text-balance sm:text-4xl">
Up and running in four steps
</h2>
<p className="mx-auto mt-4 max-w-xl text-muted-foreground">
Go from sign-up to full team collaboration without a single support
ticket.
</p>
</div>
<ol
className={cn("relative flex flex-col gap-8", "md:flex-row md:gap-0")}
>
<div
className="absolute top-0 left-[1.25rem] hidden h-full w-px bg-border md:top-5 md:left-0 md:h-px md:w-full"
aria-hidden="true"
/>
<div
className="absolute top-0 left-[1.25rem] hidden h-full w-px origin-top bg-primary transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] md:top-5 md:left-0 md:h-px md:w-auto"
style={{ transform: `scaleY(${progress / 100})` }}
aria-hidden="true"
/>
<div
className="absolute top-5 left-0 hidden h-px w-full origin-left bg-primary transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] md:block"
style={{ transform: `scaleX(${progress / 100})` }}
aria-hidden="true"
/>
{steps.map(({ icon: Icon, title, copy }, index) => {
const isDone = index <= active
const isCurrent = index === active
return (
<li
key={title}
className="relative flex flex-1 gap-4 md:flex-col md:items-center md:gap-0 md:px-3 md:text-center"
>
<button
type="button"
onClick={() => setActive(index)}
aria-current={isCurrent ? "step" : undefined}
className={cn(
"z-10 flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-full border p-0 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
isDone
? "border-primary bg-primary text-primary-foreground"
: "border-border bg-background text-muted-foreground hover:bg-muted",
isCurrent &&
"ring-2 ring-primary ring-offset-2 ring-offset-background"
)}
>
{isDone ? (
<IconPlaceholder
lucide="Check"
tabler="IconCheck"
hugeicons="Tick02Icon"
phosphor="Check"
remixicon="RiCheckLine"
className="size-4"
aria-hidden="true"
/>
) : (
<Icon className="size-4" aria-hidden="true" />
)}
<span className="sr-only">{title}</span>
</button>
<div className="md:mt-4">
<h3
className={cn(
"mt-0.5 font-heading text-sm font-semibold transition-colors",
isCurrent || isDone
? "text-foreground"
: "text-muted-foreground"
)}
>
{title}
</h3>
<p className="mt-1.5 text-sm text-muted-foreground md:mx-auto md:max-w-[16rem]">
{copy}
</p>
</div>
</li>
)
})}
</ol>
</div>
</section>
)
}
registry/templates/saas-landing-2/components/integrations.tsx
import { Button } from "@/components/ui/button"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"
type IconProps = { className?: string; size?: number | string }
const integrations = [
{ name: "Slack", Icon: (p: IconProps) => <SlackMark {...p} /> },
{ name: "Figma", Icon: (p: IconProps) => <FigmaMark {...p} /> },
{ name: "Notion", Icon: (p: IconProps) => <NotionMark {...p} /> },
{ name: "Vercel", Icon: (p: IconProps) => <VercelMark {...p} /> },
{ name: "GitHub", Icon: (p: IconProps) => <GithubMark {...p} /> },
{ name: "Google", Icon: (p: IconProps) => <GoogleMark {...p} /> },
]
export default function Integrations() {
return (
<section className="flex w-full items-center justify-center px-6 py-16 sm:py-24">
<div className="mx-auto grid w-full max-w-5xl grid-cols-1 items-center gap-12 lg:grid-cols-2">
<div className="flex flex-col items-start gap-4">
<span className="block text-xs font-semibold tracking-widest text-muted-foreground uppercase">
Integrations
</span>
<h2 className="font-heading text-3xl font-bold tracking-tight text-balance sm:text-4xl">
Works with the tools you already use
</h2>
<p className="text-base text-muted-foreground">
Connect Acme to your favorite apps in a couple of clicks. No brittle
scripts, no maintenance — just data flowing where you need it.
</p>
<Button render={<a href="#" />} nativeButton={false}>
Browse all integrations
<IconPlaceholder
lucide="ChevronRight"
tabler="IconChevronRight"
hugeicons="ArrowRight01Icon"
phosphor="CaretRight"
remixicon="RiArrowRightSLine"
data-icon="inline-end"
aria-hidden="true"
/>
</Button>
</div>
<div className="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-border bg-border sm:grid-cols-3">
{integrations.map(({ name, Icon }) => (
<div
key={name}
className="group flex aspect-[4/3] flex-col items-center justify-center gap-2.5 bg-background p-4 text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground"
>
<Icon className="size-8 shrink-0" aria-hidden="true" />
<span className="text-xs font-medium">{name}</span>
</div>
))}
</div>
</div>
</section>
)
}
type MarkProps = React.ComponentProps<"svg"> & { size?: number | string }
function FigmaMark({ size = 24, ...props }: MarkProps) {
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M5.33317 5.33333C5.33317 3.49238 6.82556 2 8.6665 2H11.9997H11.9998H15.333C17.174 2 18.6663 3.49238 18.6663 5.33333C18.6663 7.17428 17.174 8.66667 15.333 8.66667H11.9998H11.9997L11.9997 12L11.9997 15.3333V18.6667C11.9997 20.5076 10.5073 22 8.66634 22C6.82539 22 5.33301 20.5076 5.33301 18.6667C5.33301 16.8257 6.82539 15.3333 8.66634 15.3333C6.82539 15.3333 5.33301 13.841 5.33301 12C5.33301 10.1591 6.82539 8.66667 8.66634 8.66667H8.6665C6.82555 8.66667 5.33317 7.17428 5.33317 5.33333ZM11.9997 12C11.9997 13.841 13.4921 15.3333 15.333 15.3333C17.174 15.3333 18.6663 13.841 18.6663 12C18.6663 10.1591 17.174 8.66667 15.333 8.66667C13.4921 8.66667 11.9997 10.1591 11.9997 12Z" />
</svg>
)
}
function GithubMark({ size = 24, ...props }: MarkProps) {
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z" />
</svg>
)
}
function GoogleMark({ size = 24, ...props }: MarkProps) {
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M3.06364 7.50914C4.70909 4.24092 8.09084 2 12 2C14.6954 2 16.959 2.99095 18.6909 4.60455L15.8227 7.47274C14.7864 6.48185 13.4681 5.97727 12 5.97727C9.39542 5.97727 7.19084 7.73637 6.40455 10.1C6.2045 10.7 6.09086 11.3409 6.09086 12C6.09086 12.6591 6.2045 13.3 6.40455 13.9C7.19084 16.2636 9.39542 18.0227 12 18.0227C13.3454 18.0227 14.4909 17.6682 15.3864 17.0682C16.4454 16.3591 17.15 15.3 17.3818 14.05H12V10.1818H21.4181C21.5364 10.8363 21.6 11.5182 21.6 12.2273C21.6 15.2727 20.5091 17.8363 18.6181 19.5773C16.9636 21.1046 14.7 22 12 22C8.09084 22 4.70909 19.7591 3.06364 16.4909C2.38638 15.1409 2 13.6136 2 12C2 10.3864 2.38638 8.85911 3.06364 7.50914Z" />
</svg>
)
}
function NotionMark({ size = 24, ...props }: MarkProps) {
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M6.1039 5.90972C6.68754 6.38386 6.90648 6.34768 8.00238 6.27457L18.3342 5.65418C18.5533 5.65418 18.3711 5.43557 18.2981 5.39924L16.5822 4.15879C16.2534 3.90354 15.8153 3.61122 14.9758 3.68434L4.9715 4.41403C4.60665 4.45021 4.53377 4.63262 4.67909 4.77886L6.1039 5.90972ZM6.72422 8.31752L6.72422 19.1884C6.72422 19.7726 7.01618 19.9912 7.6733 19.955L19.028 19.298C19.6854 19.2619 19.7586 18.86 19.7586 18.3854V7.58753C19.7586 7.1137 19.5764 6.85816 19.1739 6.89464L7.30815 7.58753C6.87027 7.62433 6.72422 7.84337 6.72422 8.31752ZM17.9335 8.90066C18.0063 9.22933 17.9335 9.55767 17.6043 9.5946L17.0571 9.70361V17.7292C16.5822 17.9845 16.1441 18.1304 15.7791 18.1304C15.1947 18.1304 15.0484 17.9479 14.6107 17.401L11.0321 11.783V17.2186L12.1645 17.4741C12.1645 17.4741 12.1645 18.1304 11.2509 18.1304L8.73222 18.2765C8.65905 18.1304 8.73222 17.7659 8.98769 17.6929L9.64494 17.5108V10.324L8.73237 10.2509C8.6592 9.92222 8.84146 9.44837 9.35298 9.41159L12.0549 9.22946L15.7791 14.9205V9.88603L14.8296 9.77704C14.7567 9.37526 15.0484 9.08353 15.4135 9.04735L17.9335 8.90066ZM4.13151 3.4291L14.5376 2.66279C15.8155 2.55318 16.1443 2.6266 16.9475 3.21005L20.2692 5.54473C20.8173 5.9462 21 6.05551 21 6.49316V19.298C21 20.1005 20.7077 20.5751 19.6856 20.6477L7.60101 21.3775C6.83376 21.4141 6.4686 21.3047 6.0668 20.7937L3.6206 17.6199C3.18227 17.0357 3 16.5986 3 16.0873L3 4.70545C3 4.04918 3.29242 3.50177 4.13151 3.4291Z" />
</svg>
)
}
function SlackMark({ size = 24, ...props }: MarkProps) {
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M6.52739 14.5136C6.52739 15.5966 5.64264 16.4814 4.55959 16.4814C3.47654 16.4814 2.5918 15.5966 2.5918 14.5136C2.5918 13.4305 3.47654 12.5458 4.55959 12.5458H6.52739V14.5136ZM7.51892 14.5136C7.51892 13.4305 8.40366 12.5458 9.48671 12.5458C10.5698 12.5458 11.4545 13.4305 11.4545 14.5136V19.4407C11.4545 20.5238 10.5698 21.4085 9.48671 21.4085C8.40366 21.4085 7.51892 20.5238 7.51892 19.4407V14.5136ZM9.48671 6.52715C8.40366 6.52715 7.51892 5.6424 7.51892 4.55935C7.51892 3.4763 8.40366 2.59155 9.48671 2.59155C10.5698 2.59155 11.4545 3.4763 11.4545 4.55935V6.52715H9.48671ZM9.48671 7.51867C10.5698 7.51867 11.4545 8.40342 11.4545 9.48647C11.4545 10.5695 10.5698 11.4543 9.48671 11.4543H4.55959C3.47654 11.4543 2.5918 10.5695 2.5918 9.48647C2.5918 8.40342 3.47654 7.51867 4.55959 7.51867H9.48671ZM17.4732 9.48647C17.4732 8.40342 18.3579 7.51867 19.4409 7.51867C20.524 7.51867 21.4087 8.40342 21.4087 9.48647C21.4087 10.5695 20.524 11.4543 19.4409 11.4543H17.4732V9.48647ZM16.4816 9.48647C16.4816 10.5695 15.5969 11.4543 14.5138 11.4543C13.4308 11.4543 12.546 10.5695 12.546 9.48647V4.55935C12.546 3.4763 13.4308 2.59155 14.5138 2.59155C15.5969 2.59155 16.4816 3.4763 16.4816 4.55935V9.48647ZM14.5138 17.4729C15.5969 17.4729 16.4816 18.3577 16.4816 19.4407C16.4816 20.5238 15.5969 21.4085 14.5138 21.4085C13.4308 21.4085 12.546 20.5238 12.546 19.4407V17.4729H14.5138ZM14.5138 16.4814C13.4308 16.4814 12.546 15.5966 12.546 14.5136C12.546 13.4305 13.4308 12.5458 14.5138 12.5458H19.4409C20.524 12.5458 21.4087 13.4305 21.4087 14.5136C21.4087 15.5966 20.524 16.4814 19.4409 16.4814H14.5138Z" />
</svg>
)
}
function VercelMark({ size = 24, ...props }: MarkProps) {
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M23 21.6479H1L12 2.35205L23 21.6479Z" />
</svg>
)
}
registry/templates/saas-landing-2/components/pricing.tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"
const STARTER_FEATURES = [
"1 workspace",
"Up to 3 members",
"5 GB storage",
"Community support",
"Basic analytics",
]
const PRO_FEATURES = [
"Unlimited workspaces",
"Up to 50 members",
"100 GB storage",
"Priority email support",
"Advanced analytics",
"Custom domain",
"Audit logs",
]
type Billing = "monthly" | "annual"
export default function Pricing() {
const [billing, setBilling] = React.useState<Billing>("monthly")
const proPrice = billing === "annual" ? 23 : 29
return (
<section className="flex w-full items-center justify-center px-6 py-16 sm:py-24">
<div className="mx-auto w-full max-w-3xl">
<div className="flex flex-col gap-4 pb-8 sm:flex-row sm:items-end sm:justify-between">
<div className="flex flex-col gap-4">
<span className="block text-xs font-semibold tracking-widest text-muted-foreground uppercase">
Pricing
</span>
<h2 className="font-heading text-3xl font-bold tracking-tight text-balance sm:text-4xl">
Two plans. No surprises.
</h2>
</div>
<div
role="group"
aria-label="Billing period"
className="inline-flex items-center gap-1 self-start rounded-lg border border-border bg-muted/50 p-1 sm:self-auto"
>
<button
type="button"
onClick={() => setBilling("monthly")}
aria-pressed={billing === "monthly"}
className={cn(
"rounded-md px-4 py-1.5 text-sm font-medium transition-colors",
billing === "monthly"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
Monthly
</button>
<button
type="button"
onClick={() => setBilling("annual")}
aria-pressed={billing === "annual"}
className={cn(
"flex items-center gap-2 rounded-md px-4 py-1.5 text-sm font-medium transition-colors",
billing === "annual"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
Annual
<Badge className="px-1.5 text-[10px] font-semibold">-20%</Badge>
</button>
</div>
</div>
<div className="flex flex-col overflow-hidden rounded-xl ring-1 ring-border sm:flex-row">
<div className="flex flex-1 flex-col gap-6 bg-primary p-8 text-primary-foreground">
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3">
<span className="text-base font-semibold">Pro</span>
<Badge className="border-transparent bg-primary-foreground/20 text-primary-foreground">
Most Popular
</Badge>
</div>
<div className="flex items-baseline gap-1">
<span className="text-5xl font-bold tracking-tight">
${proPrice}
</span>
<span className="text-sm opacity-70">/Month</span>
</div>
<p className="text-sm opacity-75">
For growing teams that ship daily and need more room to scale.
</p>
</div>
<Separator className="bg-primary-foreground/20" />
<ul className="flex flex-1 flex-col gap-3">
{PRO_FEATURES.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm">
<IconPlaceholder
lucide="Check"
tabler="IconCheck"
hugeicons="Tick02Icon"
phosphor="Check"
remixicon="RiCheckLine"
data-icon="inline-start"
className="size-4 shrink-0 opacity-80"
aria-hidden="true"
/>
{feature}
</li>
))}
</ul>
<Button
nativeButton={false}
variant="secondary"
size="lg"
className="w-full"
render={<a href="#" />}
>
Start Free Trial
<IconPlaceholder
lucide="ArrowRight"
tabler="IconArrowRight"
hugeicons="ArrowRightIcon"
phosphor="ArrowRight"
remixicon="RiArrowRightLine"
data-icon="inline-end"
aria-hidden="true"
/>
</Button>
</div>
<div className="flex flex-1 flex-col gap-6 bg-card p-8">
<div className="flex flex-col gap-3">
<span className="text-base font-semibold">Starter</span>
<div className="flex items-baseline gap-1">
<span className="text-5xl font-bold tracking-tight">$0</span>
<span className="text-sm text-muted-foreground">/Month</span>
</div>
<p className="text-sm text-muted-foreground">
For individuals and side projects just getting off the ground.
</p>
</div>
<Separator />
<ul className="flex flex-1 flex-col gap-3">
{STARTER_FEATURES.map((feature) => (
<li
key={feature}
className="flex items-center gap-2 text-sm text-foreground"
>
<IconPlaceholder
lucide="Check"
tabler="IconCheck"
hugeicons="Tick02Icon"
phosphor="Check"
remixicon="RiCheckLine"
data-icon="inline-start"
className="size-4 shrink-0 text-muted-foreground"
aria-hidden="true"
/>
{feature}
</li>
))}
</ul>
<Button
nativeButton={false}
variant="outline"
size="lg"
className="w-full"
render={<a href="#" />}
>
Get Started Free
</Button>
</div>
</div>
<p className="mt-4 text-center text-xs text-muted-foreground">
No credit card required. Upgrade or cancel anytime.
</p>
</div>
</section>
)
}
registry/templates/saas-landing-2/components/stats.tsx
const stats = [
{ value: "99.99%", label: "Uptime across all regions" },
{ value: "12M+", label: "Requests handled every day" },
{ value: "180ms", label: "Median global response time" },
{ value: "40+", label: "Countries with edge presence" },
]
export default function Stats() {
return (
<section className="flex w-full items-center justify-center px-6 py-16 sm:py-24">
<div className="mx-auto w-full max-w-5xl">
<div className="rounded-xl bg-primary px-6 py-14 text-primary-foreground sm:px-12">
<div className="mx-auto max-w-xl text-center">
<span className="mb-4 block text-xs font-semibold tracking-widest text-primary-foreground/70 uppercase">
By The Numbers
</span>
<h2 className="font-heading text-3xl font-bold tracking-tight text-balance sm:text-4xl">
Built for scale from day one
</h2>
<p className="mt-4 text-sm text-primary-foreground/70">
The numbers behind a platform teams rely on in production.
</p>
</div>
<dl className="mt-12 grid grid-cols-2 gap-y-10 lg:grid-cols-4">
{stats.map(({ value, label }) => (
<div
key={label}
className="flex flex-col items-center gap-2 text-center"
>
<dt className="text-4xl font-bold tracking-tight tabular-nums sm:text-5xl">
{value}
</dt>
<dd className="max-w-[12rem] text-xs text-primary-foreground/70">
{label}
</dd>
</div>
))}
</dl>
</div>
</div>
</section>
)
}
registry/templates/saas-landing-2/components/testimonials.tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Separator } from "@/components/ui/separator"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"
const testimonial = {
quote:
"Acme's platform compressed what used to be a six-week integration cycle down to a single sprint. Our engineers finally have breathing room to work on things that matter, and our customers felt the difference immediately.",
name: "Isabelle Fontaine",
role: "Chief Product Officer",
company: "Meridian Labs",
initials: "IF",
avatar: "https://i.pravatar.cc/150?img=45",
}
const metrics = [
{ value: "6×", label: "faster integrations" },
{ value: "40%", label: "less engineer toil" },
{ value: "1 sprint", label: "to full deployment" },
]
export default function Testimonials() {
return (
<section className="flex w-full items-center justify-center px-6 py-16 sm:py-24">
<div className="mx-auto w-full max-w-2xl">
<span className="mb-4 block text-xs font-semibold tracking-widest text-muted-foreground uppercase">
Customer Story
</span>
<div className="relative rounded-xl border border-border bg-card p-10 sm:p-14">
<div
className="absolute top-10 left-0 h-16 w-[3px] bg-foreground"
aria-hidden="true"
/>
<IconPlaceholder
lucide="Quote"
tabler="IconQuotes"
hugeicons="QuotesIcon"
phosphor="Quotes"
remixicon="RiDoubleQuotesR"
className="mb-8 size-8 text-muted-foreground"
aria-hidden="true"
/>
<blockquote className="text-xl leading-[1.65] font-medium tracking-tight text-foreground sm:text-2xl">
“{testimonial.quote}”
</blockquote>
<Separator className="my-10" />
<div className="flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-4">
<Avatar className="size-14 border border-border">
<AvatarImage
src={testimonial.avatar}
alt={testimonial.name}
className="grayscale"
/>
<AvatarFallback className="text-sm font-semibold">
{testimonial.initials}
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1">
<span className="text-sm leading-tight font-semibold text-foreground">
{testimonial.name}
</span>
<span className="text-xs leading-tight text-muted-foreground">
{testimonial.role},{" "}
<span className="font-medium text-foreground">
{testimonial.company}
</span>
</span>
</div>
</div>
<div className="flex items-center gap-6 sm:gap-8">
{metrics.map((m, i) => (
<div
key={i}
className="flex flex-col items-center gap-0.5 text-center"
>
<span className="text-lg leading-none font-bold tracking-tight text-foreground">
{m.value}
</span>
<span className="text-[11px] leading-tight text-muted-foreground">
{m.label}
</span>
</div>
))}
</div>
</div>
</div>
</div>
</section>
)
}