# Pricing 2

> Center-stage plan panel with chip selector, billing toggle, and blur handoff — witness motion adapted for pricing.

A shadcn/ui block from the [@nusaiba](https://nusaiba.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/nusaiba/pricing-2).

- Page, with a live preview: https://shadcn.exchange/r/nusaiba/pricing-2
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/nusaiba/pricing-2-light-a5894615737f79b9.png
- Registry item: https://nusaiba.dev/r/base-nova/pricing-2.json
- Categories: pricing
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://nusaiba.dev/r/base-nova/pricing-2.json
```

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

## Dependencies

- npm packages: `motion`
- Registry items: `@nusaiba/foundation`, `button`, `badge`

## Files

### `registry/blocks/pricing/2/pricing-section.tsx`

```tsx
"use client";

import { useCallback, useEffect, useState } from "react";
import { CheckIcon } from "lucide-react";
import { AnimatePresence, motion, useInView, useReducedMotion } from "motion/react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { defaultSpring, motionStagger, motionViewport } from "@/lib/motion-tokens";
import { cn } from "@/lib/utils";
import { SectionShell, ContentRail } from "@/components/layout-contract";

type BillingPeriod = "monthly" | "annual";

type Plan = {
	name: string;
	tag: string;
	description: string;
	features: string[];
	popular?: boolean;
	custom?: boolean;
	monthly: { amount: string; suffix: string };
	annual: { amount: string; suffix: string };
};

const plans: Plan[] = [
	{
		name: "Starter",
		tag: "solo",
		description: "Install free blocks, wire your own backend, ship at your pace.",
		features: ["All free registry blocks", "Shadcn CLI install", "Community support"],
		monthly: { amount: "$0", suffix: "forever" },
		annual: { amount: "$0", suffix: "forever" },
	},
	{
		name: "Pro",
		tag: "team",
		description: "Pro blocks, lifetime updates, and a token for private registry installs.",
		features: [
			"Every Pro block",
			"Lifetime catalog updates",
			"Commercial license",
			"Registry token",
		],
		popular: true,
		monthly: { amount: "$147", suffix: "once" },
		annual: { amount: "$117", suffix: "once · save 20%" },
	},
	{
		name: "Enterprise",
		tag: "org",
		description: "Seats, SSO, and a motion audit for teams standardizing on the registry.",
		features: [
			"Everything in Pro",
			"Team workspace",
			"Role-based access",
			"Priority support",
		],
		custom: true,
		monthly: { amount: "Custom", suffix: "annual contract" },
		annual: { amount: "Custom", suffix: "annual contract" },
	},
];

const CYCLE_MS = 4200;
const DEFAULT_ACTIVE = 1;

function BillingToggle({
	period,
	onChange,
	reduceMotion,
}: {
	period: BillingPeriod;
	onChange: (period: BillingPeriod) => void;
	reduceMotion: boolean;
}) {
	return (
		<div
			className="relative inline-flex rounded-full border border-border bg-muted/30 p-1"
			role="group"
			aria-label="Billing period"
		>
			{(["monthly", "annual"] as const).map((option) => {
				const isActive = period === option;
				return (
					<button
						aria-pressed={isActive}
						className={cn(
							"relative z-10 rounded-full px-4 py-2 font-medium text-sm transition-colors active:scale-[0.96]",
							"after:absolute after:top-1/2 after:left-1/2 after:size-10 after:-translate-1/2",
							isActive ? "text-foreground" : "text-muted-foreground",
						)}
						key={option}
						onClick={() => onChange(option)}
						type="button"
					>
						{isActive ? (
							reduceMotion ? (
								<span className="absolute inset-0 -z-10 rounded-full bg-background shadow-[0_1px_2px_rgba(0,0,0,0.06)] dark:shadow-[0_1px_2px_rgba(0,0,0,0.25)]" />
							) : (
								<motion.span
									className="absolute inset-0 -z-10 rounded-full bg-background shadow-[0_1px_2px_rgba(0,0,0,0.06)] dark:shadow-[0_1px_2px_rgba(0,0,0,0.25)]"
									layoutId="pricing-2-billing-pill"
									transition={{ type: "spring", duration: 0.3, bounce: 0 }}
								/>
							)
						) : null}
						{option === "monthly" ? "Monthly" : "Annual"}
					</button>
				);
			})}
		</div>
	);
}

function PlanPrice({
	plan,
	period,
	reduceMotion,
	large,
}: {
	plan: Plan;
	period: BillingPeriod;
	reduceMotion: boolean;
	large?: boolean;
}) {
	const pricing = period === "monthly" ? plan.monthly : plan.annual;

	return (
		<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
			<AnimatePresence initial={false} mode="popLayout">
				<motion.span
					animate={reduceMotion ? undefined : { opacity: 1, y: 0, filter: "blur(0px)" }}
					className={cn(
						"font-medium tabular-nums tracking-tight",
						large ? "text-4xl md:text-5xl" : "text-2xl",
					)}
					exit={
						reduceMotion
							? undefined
							: {
									opacity: 0,
									y: -6,
									filter: "blur(4px)",
									transition: { duration: 0.15, ease: "easeIn" },
								}
					}
					initial={reduceMotion ? false : { opacity: 0, y: 8, filter: "blur(4px)" }}
					key={`${plan.name}-${period}-${pricing.amount}`}
					transition={{ type: "spring", duration: 0.3, bounce: 0 }}
				>
					{pricing.amount}
				</motion.span>
			</AnimatePresence>
			<span className="text-muted-foreground text-sm">{pricing.suffix}</span>
		</div>
	);
}

function PlanChip({
	plan,
	isActive,
	onSelect,
	reduceMotion,
}: {
	plan: Plan;
	isActive: boolean;
	onSelect: () => void;
	reduceMotion: boolean;
}) {
	return (
		<button
			aria-label={`Show ${plan.name} plan`}
			aria-pressed={isActive}
			className={cn(
				"relative flex min-h-10 items-center gap-2 rounded-[var(--radius-md)] px-3 py-2 transition-[background-color,box-shadow,transform] active:scale-[0.96]",
				"after:absolute after:top-1/2 after:left-1/2 after:size-10 after:-translate-1/2",
				isActive
					? "bg-muted/50 shadow-[0_0_0_1px_rgba(0,0,0,0.06)] dark:shadow-[0_0_0_1px_rgba(255,255,255,0.08)]"
					: "bg-transparent hover:bg-muted/25",
			)}
			onClick={onSelect}
			type="button"
		>
			<motion.span
				animate={
					reduceMotion
						? undefined
						: {
								opacity: isActive ? 1 : 0.5,
								filter: isActive ? "blur(0px)" : "blur(1px)",
								scale: isActive ? 1 : 0.94,
							}
				}
				className="font-mono text-[10px] text-muted-foreground uppercase tracking-[0.14em]"
				transition={{ type: "spring", duration: 0.3, bounce: 0 }}
			>
				{plan.tag}
			</motion.span>
			<span className="font-medium text-sm">{plan.name}</span>
		</button>
	);
}

export function PricingToggleSection() {
	const reduceMotion = useReducedMotion();
	const [rootNode, setRootNode] = useState<HTMLElement | null>(null);
	const isInView = useInView(
		{ current: rootNode },
		{ ...motionViewport, amount: 0.35 },
	);
	const [period, setPeriod] = useState<BillingPeriod>("monthly");
	const [active, setActive] = useState(DEFAULT_ACTIVE);
	const [cycleActive, setCycleActive] = useState(false);

	const activePlan = plans[active]!;

	useEffect(() => {
		if (!isInView || reduceMotion) return;
		setCycleActive(true);
	}, [isInView, reduceMotion]);

	useEffect(() => {
		if (!cycleActive || reduceMotion) return;
		const timer = setInterval(() => {
			setActive((current) => (current + 1) % plans.length);
		}, CYCLE_MS);
		return () => clearInterval(timer);
	}, [cycleActive, reduceMotion]);

	const selectPlan = useCallback((index: number) => {
		setActive(index);
		setCycleActive(false);
		setTimeout(() => setCycleActive(true), CYCLE_MS);
	}, []);

	return (
		<SectionShell spacingMode="section" className="bg-background" ref={setRootNode}>
			<ContentRail maxWidth="max-w-3xl" className="space-y-10 md:space-y-12">
				<div className="mx-auto max-w-xl space-y-6 text-center">
					<div className="space-y-2">
						<p className="font-medium text-primary text-xs uppercase tracking-[0.22em]">
							Billing
						</p>
						<h2 className="text-balance font-medium text-2xl tracking-tight md:text-4xl">
							One plan in focus
						</h2>
						<p className="text-pretty text-muted-foreground text-sm md:text-base">
							Cycle through tiers or pick a chip — prices shift when you toggle annual
							billing.
						</p>
					</div>
					<BillingToggle
						onChange={setPeriod}
						period={period}
						reduceMotion={!!reduceMotion}
					/>
				</div>

				<div className="relative">
					<motion.div
						animate={
							reduceMotion
								? undefined
								: { opacity: [0.35, 0.55, 0.35], scale: [1, 1.03, 1] }
						}
						aria-hidden
						className="pointer-events-none absolute inset-x-6 top-1/2 -z-10 h-48 -translate-y-1/2 rounded-full bg-primary/10 blur-3xl md:inset-x-12 md:h-56"
						transition={{
							duration: 4.5,
							repeat: Number.POSITIVE_INFINITY,
							ease: "easeInOut",
						}}
					/>

					<motion.div
						className="relative overflow-hidden border border-border bg-background p-6 shadow-[0_1px_2px_rgba(0,0,0,0.04)] md:p-10 dark:shadow-[0_1px_2px_rgba(0,0,0,0.2)]"
						initial={reduceMotion ? false : { opacity: 0, y: 16, filter: "blur(4px)" }}
						transition={{ ...defaultSpring, delay: motionStagger.item }}
						viewport={motionViewport}
						whileInView={reduceMotion ? undefined : { opacity: 1, y: 0, filter: "blur(0px)" }}
					>
						<AnimatePresence initial={false} mode="wait">
							<motion.div
								animate={{ opacity: 1, filter: "blur(0px)", y: 0 }}
								className="space-y-6"
								exit={{
									opacity: 0,
									filter: "blur(6px)",
									y: -10,
									transition: { duration: 0.18, ease: "easeIn" },
								}}
								initial={{ opacity: 0, filter: "blur(6px)", y: 8 }}
								key={activePlan.name}
								transition={{ type: "spring", duration: 0.35, bounce: 0 }}
							>
								<div className="flex flex-wrap items-start justify-between gap-3">
									<div className="space-y-1">
										<div className="flex flex-wrap items-center gap-2">
											<p className="font-medium text-lg md:text-xl">{activePlan.name}</p>
											{activePlan.popular ? <Badge>Most picked</Badge> : null}
										</div>
										<p className="font-mono text-[10px] text-muted-foreground uppercase tracking-[0.16em]">
											{activePlan.tag} tier
										</p>
									</div>
									<PlanPrice
										large
										period={period}
										plan={activePlan}
										reduceMotion={!!reduceMotion}
									/>
								</div>

								<p className="max-w-lg text-pretty text-muted-foreground text-sm leading-relaxed md:text-base">
									{activePlan.description}
								</p>

								<ul className="grid gap-3 sm:grid-cols-2">
									{activePlan.features.map((feature, index) => (
										<motion.li
											animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
											className="flex items-start gap-2 text-sm"
											initial={
												reduceMotion
													? false
													: { opacity: 0, y: 6, filter: "blur(4px)" }
											}
											key={feature}
											transition={{
												type: "spring",
												duration: 0.3,
												bounce: 0,
												delay: 0.04 + index * motionStagger.item,
											}}
										>
											<CheckIcon className="mt-0.5 size-4 shrink-0 text-primary" />
											{feature}
										</motion.li>
									))}
								</ul>

								<Button
									className="w-full active:scale-[0.96] sm:w-auto"
									variant={activePlan.popular ? "default" : "outline"}
								>
									{activePlan.custom ? "Talk to us" : "Get started"}
								</Button>
							</motion.div>
						</AnimatePresence>
					</motion.div>
				</div>

				<ul className="flex flex-wrap items-center justify-center gap-2 md:gap-2.5">
					{plans.map((plan, index) => (
						<li key={plan.name}>
							<PlanChip
								isActive={index === active}
								onSelect={() => selectPlan(index)}
								plan={plan}
								reduceMotion={!!reduceMotion}
							/>
						</li>
					))}
				</ul>
			</ContentRail>
		</SectionShell>
	);
}
```
