A pricing section for an AI dubbing tool, priced by the minutes you dub. Drag the usage slider and four plan cards measure it against their allowances: past one, its overage shows as a hatched stretch and its price climbs, until the highlight glides to the plan that costs less. Monthly and yearly billing count every price over, and a volume card beside "In every plan" lights up at the slider's far end.
registry/blocks/plan-fit-pricing.tsx
"use client";
import * as React from "react";
import { PlanFit, type PlanFitBilling, type PlanFitPlan } from "@/components/ui/plan-fit";
/* ── Content ─────────────────────────────────────────────────
* Everything you'd change to make the section yours: the copy,
* the plans and their allowances, what every plan includes and
* the volume card. The slider picks the plan that costs least
* for the minutes a visitor dubs; drag it to the far end and
* the volume card lights up.
* ─────────────────────────────────────────────────────────── */
/** The one accent: the slider, the best plan's highlight and button, and the headline's last words. */
const ACCENT = { light: "#ff4a17", dark: "#ff6a3d" };
const EYEBROW = "Pricing";
const HEADLINE = { before: "Pay for the minutes", accent: "you dub." };
const SUBLINE =
"Every plan speaks 32 languages in your own voice. Drag to how much video you publish a month and the plan that costs least lights up.";
const UNIT = "minutes";
const READOUT = "minutes of video dubbed per month";
const START = 90;
const MAX = 6000;
const BILLING: PlanFitBilling[] = [
{ id: "monthly", label: "Monthly" },
{ id: "yearly", label: "Yearly", note: "Save 20%" },
];
const PLANS: PlanFitPlan[] = [
{
id: "free",
name: "Free",
pitch: "Hear yourself in another language.",
price: 0,
included: 10,
caption: "No card needed",
features: ["3 languages", "720p export", "Mynah watermark"],
cta: { label: "Start free", href: "#signup" },
},
{
id: "creator",
name: "Creator",
pitch: "For a channel that publishes every week.",
price: { monthly: 24, yearly: 19 },
included: 120,
overage: { price: 8, per: 30 },
caption: { monthly: "Billed monthly", yearly: "Billed $228 a year" },
features: ["32 languages", "1 cloned voice", "1080p, no watermark"],
cta: { label: "Start dubbing", href: "#creator" },
},
{
id: "studio",
name: "Studio",
pitch: "For teams localizing a whole catalog.",
price: { monthly: 99, yearly: 79 },
included: 600,
overage: { price: 30, per: 150 },
caption: { monthly: "Billed monthly", yearly: "Billed $948 a year" },
features: ["5 cloned voices", "Lip sync", "Review links for translators"],
cta: { label: "Choose Studio", href: "#studio" },
},
{
id: "network",
name: "Network",
pitch: "For publishers releasing daily, everywhere.",
price: { monthly: 399, yearly: 319 },
included: 3000,
overage: { price: 120, per: 750 },
caption: { monthly: "Billed monthly", yearly: "Billed $3,828 a year" },
features: ["Unlimited voices", "API and webhooks", "Priority render queue"],
cta: { label: "Choose Network", href: "#network" },
},
];
const INCLUDED = {
title: "In every plan",
items: [
{ title: "Your voice, cloned", body: "From a minute of clean audio." },
{ title: "Lip-matched edits", body: "Fix a line and only that line re-renders." },
{ title: "Music and effects kept", body: "Only the speech is replaced." },
{ title: "Subtitles included", body: "SRT and VTT for every language." },
],
};
/** The card beside "In every plan". It lights up when the slider reaches `from` minutes. */
const VOLUME = {
from: MAX,
title: "Over 6,000 minutes a month?",
body: "Volume pricing, custom voice models trained on your talent, a dedicated render queue and SSO.",
cta: { label: "Talk to sales", href: "#sales" },
link: { label: "Read the security brief", href: "#security" },
};
/* ── Section ─────────────────────────────────────────────── */
export function PlanFitPricing() {
const [volume, setVolume] = React.useState(false);
return (
<section
aria-labelledby="plan-fit-pricing-title"
style={{ "--accent-light": ACCENT.light, "--accent-dark": ACCENT.dark } as React.CSSProperties}
className="bg-[#fafafa] text-zinc-950 [--accent:var(--accent-light)] dark:bg-[#09090b] dark:text-white dark:[--accent:var(--accent-dark)]"
>
<div className="mx-auto max-w-6xl px-6 pt-16 pb-24 sm:pt-20 sm:pb-28">
<div className="mx-auto max-w-2xl text-center">
<p className="inline-flex items-center gap-2 rounded-full bg-white px-3 py-1 text-xs font-semibold shadow-[0_0_0_1px_rgb(9_9_11/0.08)] dark:bg-white/[0.06] dark:shadow-[0_0_0_1px_rgb(255_255_255/0.1)]">
<span aria-hidden className="size-1.5 rounded-full bg-(--accent)" />
{EYEBROW}
</p>
<h2
id="plan-fit-pricing-title"
className="mt-5 text-4xl/[1.05] font-semibold tracking-[-0.04em] text-balance sm:text-[3.5rem]/[1.02]"
>
{HEADLINE.before} <span className="text-(--accent)">{HEADLINE.accent}</span>
</h2>
<p className="mx-auto mt-5 max-w-xl text-base/7 text-pretty text-zinc-600 dark:text-zinc-400">{SUBLINE}</p>
</div>
<PlanFit
plans={PLANS}
unit={UNIT}
readoutLabel={READOUT}
defaultValue={START}
max={MAX}
billing={BILLING}
onValueChange={(usage) => setVolume(usage >= VOLUME.from)}
className="mt-12 [--plan-fit-accent:var(--accent)] [--plan-fit-surface:#ffffff] dark:[--plan-fit-surface:#0f0f12]"
/>
<div className="mt-6 grid gap-6 lg:grid-cols-5">
<div className="rounded-2xl bg-white p-6 shadow-[0_0_0_1px_rgb(9_9_11/0.09)] sm:p-8 lg:col-span-3 dark:bg-[#0f0f12] dark:shadow-[0_0_0_1px_rgb(255_255_255/0.1)]">
<h3 className="text-[15px] font-semibold tracking-[-0.01em]">{INCLUDED.title}</h3>
<ul className="mt-5 grid gap-x-8 gap-y-5 sm:grid-cols-2">
{INCLUDED.items.map((item) => (
<li key={item.title} className="flex gap-3">
<span
aria-hidden
className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-(--accent)/12 text-(--accent)"
>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth={2} className="size-3">
<path d="m3.5 8.5 3 3 6-7" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
<span className="min-w-0">
<span className="block text-sm font-medium">{item.title}</span>
<span className="mt-0.5 block text-sm text-zinc-500 dark:text-zinc-400">{item.body}</span>
</span>
</li>
))}
</ul>
</div>
{/* Volume: a night card in both schemes. Its ring takes the accent once the slider reaches the far end. */}
<div
data-active={volume || undefined}
className="flex flex-col rounded-2xl bg-zinc-950 p-6 text-white shadow-[0_0_0_1px_rgb(255_255_255/0.06)] transition-shadow duration-200 ease-out data-active:shadow-[0_0_0_2px_var(--accent),0_24px_48px_-24px_var(--accent)] sm:p-8 lg:col-span-2 dark:bg-[#141418] dark:shadow-[0_0_0_1px_rgb(255_255_255/0.1)]"
>
<h3 className="text-[15px] font-semibold tracking-[-0.01em]">{VOLUME.title}</h3>
<p className="mt-2 text-sm/6 text-pretty text-zinc-400">{VOLUME.body}</p>
<div className="mt-6 flex flex-wrap items-center gap-x-5 gap-y-3 lg:mt-auto lg:pt-6">
<a
href={VOLUME.cta.href}
className="inline-flex h-9 items-center rounded-full bg-white px-4 text-[13px] font-semibold text-zinc-950 transition-[background-color,scale] duration-150 ease-out hover:bg-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) active:scale-[0.97]"
>
{VOLUME.cta.label}
</a>
<a
href={VOLUME.link.href}
className="rounded-sm text-[13px] font-medium text-zinc-300 underline decoration-white/30 underline-offset-4 transition-[color,text-decoration-color] duration-150 hover:text-white hover:decoration-white focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--accent)"
>
{VOLUME.link.label}
</a>
</div>
</div>
</div>
</div>
</section>
);
}
Live preview and the components it's built with: https://keyframeui.com/blocks/pricing-sections/plan-fit-pricing
