Workspace / block / pricing
Editorial Pricing
paxfx add blocks/block-pricing-editorial-11local onlyTier: low. Reduced motion: Fully static.
"use client";
import { cn, useInView, useReducedMotion, type BlockCopy } from "@pax-sh/blocks";
export interface PricingEditorialRate {
name: string;
audience: string;
price: string;
cadence: string;
note: string;
}
export interface PricingEditorialProps {
copy?: BlockCopy;
rates?: PricingEditorialRate[];
footnote?: string;
className?: string;
}
const DEFAULT_COPY: BlockCopy = {
eyebrow: "Rate card",
title: "Rates, stated plainly",
description:
"No cards, no badges, no most-popular nudge. The prices, set the way a magazine sets its back pages.",
};
const DEFAULT_RATES: PricingEditorialRate[] = [
{
name: "Solo",
audience: "Personal projects and open source",
price: "$0",
cadence: "forever",
note: "MIT licensed, no card required",
},
{
name: "Studio",
audience: "Freelancers and small product teams",
price: "$29",
cadence: "per editor, monthly",
note: "Premium catalog and agent registry",
},
{
name: "Agency",
audience: "Client work across many brands",
price: "$99",
cadence: "per team, monthly",
note: "White-label, multi-brand token sets",
},
{
name: "Site license",
audience: "Organizations standardizing on Pax",
price: "$490",
cadence: "per year",
note: "Unlimited editors, invoiced annually",
},
];
/**
* Pricing set like a rate card in a magazine's back pages: hairline rules
* between rows, small caps labels, tabular numerals for every figure, and
* zero cards. Rows settle in with a slight stagger, skipped under reduced
* motion where the section is fully static.
*/
export function PricingEditorial({
copy = DEFAULT_COPY,
rates = DEFAULT_RATES,
footnote = "All rates in USD. Taxes where applicable. Cancel monthly plans any time; installed blocks are yours to keep.",
className,
}: PricingEditorialProps) {
const reduced = useReducedMotion();
const [ref, inView] = useInView<HTMLDivElement>({ threshold: 0.2 });
const settled = reduced || inView;
const enter = (step: number) =>
reduced
? undefined
: ({
opacity: settled ? 1 : 0,
transform: settled ? "translateY(0)" : "translateY(8px)",
transition: `opacity 0.5s ease ${step * 0.07}s, transform 0.55s cubic-bezier(0.22, 1, 0.36, 1) ${step * 0.07}s`,
} as const);
return (
<section
className={cn(
"bg-[var(--pax-paper,#f6f2ea)] px-6 py-20 text-[var(--pax-ink,#12110f)] md:px-10 md:py-28",
className,
)}
data-pax-asset="block-pricing-editorial-11"
>
<div ref={ref} className="mx-auto max-w-6xl">
<div className="max-w-2xl" style={enter(0)}>
{copy.eyebrow ? (
<p className="m-0 text-xs font-medium uppercase tracking-[0.24em] text-[var(--pax-mute,#6b645c)] [font-variant-caps:all-small-caps]">
{copy.eyebrow}
</p>
) : null}
<h2 className="mt-3 text-3xl font-semibold tracking-tight [text-wrap:balance] md:text-4xl">
{copy.title}
</h2>
{copy.description ? (
<p className="mt-4 text-base leading-relaxed text-[var(--pax-mute,#6b645c)]">
{copy.description}
</p>
) : null}
</div>
<dl className="mt-14 border-t-2 border-[var(--pax-ink,#12110f)]">
{rates.map((rate, index) => (
<div
key={rate.name}
className="grid grid-cols-1 gap-2 border-b border-[var(--pax-border,rgba(18,17,15,0.12))] py-6 md:grid-cols-[minmax(0,200px)_1fr_auto] md:items-baseline md:gap-8"
style={enter(index + 1)}
>
<dt className="text-sm font-semibold uppercase tracking-[0.18em]">{rate.name}</dt>
<dd className="m-0">
<p className="m-0 text-sm text-[var(--pax-mute,#6b645c)]">{rate.audience}</p>
<p className="m-0 mt-1 text-xs uppercase tracking-[0.14em] text-[var(--pax-mute,#6b645c)]">
{rate.note}
</p>
</dd>
<dd className="m-0 text-right md:text-left">
<span className="text-2xl font-semibold tracking-tight [font-variant-numeric:tabular-nums]">
{rate.price}
</span>
<span className="ml-2 text-xs uppercase tracking-[0.14em] text-[var(--pax-mute,#6b645c)]">
{rate.cadence}
</span>
</dd>
</div>
))}
</dl>
<p
className="mt-6 max-w-xl text-xs leading-relaxed text-[var(--pax-mute,#6b645c)]"
style={enter(rates.length + 1)}
>
{footnote}
</p>
</div>
</section>
);
}