# Plan Fit CTA

> A closing call to action on a night band. Beside the headline, a plan picker on its own card: drag to how much video you publish and the cards swipe to the plan that fits, and the main button changes to name it.

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

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

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/plan-fit-cta.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/cta-sections/plan-fit-cta

## Files

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

```tsx
"use client";

import * as React from "react";

import { PlanFit, type PlanFitPlan } from "@/components/ui/plan-fit";

/* ── Content ─────────────────────────────────────────────────
 * Everything you'd change to make the section yours: the copy,
 * the plans and the button. The section is a night band in both
 * schemes; the plan picker sits on its own card beside the copy,
 * and the main button always names the plan that fits.
 * ─────────────────────────────────────────────────────────── */

/** The one accent: the slider, the best plan's highlight and the headline's last word. */
const ACCENT = "#ff5a26";

const HEADLINE = { before: "Your first ten minutes are", accent: "on us." };
const BODY =
  "Upload a video, pick a language and hear yourself speak it before you choose a plan. Move to a bigger one the month you outgrow it.";
const POINTS = ["No card to start", "Cancel in two clicks", "Keep every dub you made"];
const CTA = { label: (plan: string) => `Start with ${plan}`, href: "#signup" };
const SECONDARY = { label: "Watch a 2-minute demo", href: "#demo" };

const PICKER = { title: "How much do you publish?", unit: "minutes", readout: "minutes of video a month", start: 240 };

const PLANS: PlanFitPlan[] = [
  {
    id: "creator",
    name: "Creator",
    pitch: "For a channel that publishes every week.",
    price: 24,
    included: 120,
    overage: { price: 8, per: 30 },
    caption: "Billed monthly",
    features: ["32 languages", "1 cloned voice"],
  },
  {
    id: "studio",
    name: "Studio",
    pitch: "For teams localizing a whole catalog.",
    price: 99,
    included: 600,
    overage: { price: 30, per: 150 },
    caption: "Billed monthly",
    features: ["5 cloned voices", "Lip sync"],
  },
  {
    id: "network",
    name: "Network",
    pitch: "For publishers releasing daily.",
    price: 399,
    included: 3000,
    caption: "Billed monthly",
    features: ["Unlimited voices", "API and webhooks"],
  },
];

/* ── Section ─────────────────────────────────────────────── */

export function PlanFitCta() {
  // PlanFit reports the plan that fits on mount and whenever it changes.
  const [plan, setPlan] = React.useState(PLANS[0].id);
  const name = PLANS.find((p) => p.id === plan)?.name ?? PLANS[0].name;

  return (
    <section
      aria-labelledby="plan-fit-cta-title"
      style={{ "--accent": ACCENT } as React.CSSProperties}
      className="bg-[#09090b] text-white dark:bg-[#050506]"
    >
      <div className="mx-auto grid max-w-6xl items-center gap-12 px-6 py-24 sm:py-28 lg:grid-cols-[1fr_27rem] lg:gap-16">
        <div>
          <h2
            id="plan-fit-cta-title"
            className="max-w-[14ch] text-4xl/[1.05] font-semibold tracking-[-0.04em] text-balance sm:text-6xl/[1.02]"
          >
            {HEADLINE.before} <span className="text-(--accent)">{HEADLINE.accent}</span>
          </h2>
          <p className="mt-6 max-w-md text-base/7 text-pretty text-zinc-400">{BODY}</p>
          <ul className="mt-6 flex flex-wrap gap-x-5 gap-y-2 text-sm text-zinc-300">
            {POINTS.map((point) => (
              <li key={point} className="flex items-center gap-2">
                <svg
                  viewBox="0 0 16 16"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth={2}
                  aria-hidden
                  className="size-3.5 text-(--accent)"
                >
                  <path d="m3.5 8.5 3 3 6-7" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
                {point}
              </li>
            ))}
          </ul>
          <div className="mt-9 flex flex-wrap items-center gap-x-6 gap-y-4">
            <a
              href={CTA.href}
              className="inline-flex h-12 items-center gap-2 rounded-full bg-(--accent) px-6 text-[15px] font-semibold text-zinc-950 transition-[filter,scale] duration-150 ease-out hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white active:scale-[0.97]"
            >
              {CTA.label(name)}
              <svg viewBox="0 0 16 16" fill="none" aria-hidden className="size-4">
                <path
                  d="M3 8h10m-4-4 4 4-4 4"
                  stroke="currentColor"
                  strokeWidth="1.7"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
              </svg>
            </a>
            <a
              href={SECONDARY.href}
              className="rounded-sm text-sm 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-white"
            >
              {SECONDARY.label}
            </a>
          </div>
        </div>

        {/* The picker on its own card: white in light mode, raised night in dark. */}
        <div className="rounded-[1.75rem] bg-white p-6 text-zinc-950 shadow-[0_40px_80px_-40px_rgb(0_0_0/0.8)] sm:p-7 dark:bg-[#141418] dark:text-white dark:shadow-[0_0_0_1px_rgb(255_255_255/0.08),0_40px_80px_-40px_rgb(0_0_0/0.9)]">
          <p className="text-[15px] font-semibold tracking-[-0.01em]">{PICKER.title}</p>
          <PlanFit
            plans={PLANS}
            unit={PICKER.unit}
            readoutLabel={PICKER.readout}
            defaultValue={PICKER.start}
            onFitChange={setPlan}
            className="mt-5 [--plan-fit-accent:var(--accent)] [--plan-fit-surface:#ffffff] dark:[--plan-fit-surface:#141418]"
          />
        </div>
      </div>
    </section>
  );
}
```
