Fazekit
Catalog

Code

"use client";
import * as React from "react";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";

export type Plan = {
  name: string;
  description: string;
  monthly: number;
  yearly: number;
  features: string[];
  cta: string;
  highlighted?: boolean;
};

export interface PricingPlansProps {
  plans: Plan[];
  currency?: string;
  onSelect?: (plan: Plan, cycle: "monthly" | "yearly") => void;
  className?: string;
}

export function PricingPlans({ plans, currency = "USD", onSelect, className }: PricingPlansProps) {
  const [cycle, setCycle] = React.useState<"monthly" | "yearly">("yearly");
  const fmt = new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 0 });
  const maxSave = Math.max(...plans.map((p) => (p.monthly ? Math.round((1 - p.yearly / (p.monthly * 12)) * 100) : 0)));

  return (
    <section className={cn("w-full", className)}>
      <div className="mb-10 flex justify-center">
        <div role="radiogroup" aria-label="Billing cycle" className="relative inline-flex rounded-full border bg-muted/50 p-1 text-sm">
          {(["monthly", "yearly"] as const).map((c) => (
            <button
              key={c}
              role="radio"
              aria-checked={cycle === c}
              onClick={() => setCycle(c)}
              className={cn("relative z-10 rounded-full px-4 py-1.5 font-medium capitalize transition", cycle === c ? "bg-background shadow-sm" : "text-muted-foreground")}
            >
              {c}
              {c === "yearly" && maxSave > 0 && (
                <span className="ml-1.5 rounded-full bg-emerald-500/15 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-600 dark:text-emerald-400">-{maxSave}%</span>
              )}
            </button>
          ))}
        </div>
      </div>

      <div className="mx-auto grid max-w-5xl gap-6 md:grid-cols-3">
        {plans.map((p) => {
          const price = cycle === "monthly" ? p.monthly : p.yearly / 12;
          return (
            <div
              key={p.name}
              className={cn(
                "relative flex flex-col rounded-2xl border bg-card p-6 text-card-foreground",
                p.highlighted && "border-primary shadow-xl shadow-primary/10 md:-translate-y-2",
              )}
            >
              {p.highlighted && (
                <span className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-primary px-3 py-1 text-xs font-medium text-primary-foreground">Most popular</span>
              )}
              <h3 className="font-semibold">{p.name}</h3>
              <p className="mt-1 text-sm text-muted-foreground">{p.description}</p>
              <p className="mt-6 flex items-baseline gap-1">
                <span className="text-4xl font-semibold tracking-tight tabular-nums">{fmt.format(price)}</span>
                <span className="text-sm text-muted-foreground">/mo</span>
              </p>
              <p className="h-5 text-xs text-muted-foreground">{cycle === "yearly" && p.yearly > 0 ? `${fmt.format(p.yearly)} billed yearly` : ""}</p>
              <button
                onClick={() => onSelect?.(p, cycle)}
                className={cn(
                  "mt-6 h-10 rounded-lg text-sm font-medium transition",
                  p.highlighted ? "bg-primary text-primary-foreground hover:bg-primary/90" : "border hover:bg-accent",
                )}
              >
                {p.cta}
              </button>
              <ul className="mt-6 space-y-2.5 text-sm">
                {p.features.map((f) => (
                  <li key={f} className="flex gap-2">
                    <Check className="mt-0.5 size-4 shrink-0 text-primary" />
                    <span>{f}</span>
                  </li>
                ))}
              </ul>
            </div>
          );
        })}
      </div>
    </section>
  );
}