# Plan Fit Pricing

> 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.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/plan-fit-pricing).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/plan-fit-pricing
- Screenshot: https://shadcn.exchange/shot/keyframeui/plan-fit-pricing/light.png
- Registry item: https://keyframeui.com/r/plan-fit-pricing.json
- Categories: blocks, pricing-sections
- Listed: 2026-11-02, source last changed 2026-11-02

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/plan-fit-pricing.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/plan-fit.json`

## Notes from the registry

Live preview and the components it's built with: https://keyframeui.com/blocks/pricing-sections/plan-fit-pricing

## Files

### `registry/blocks/plan-fit-pricing.tsx`

```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>
  );
}
```
