Workspace / block / pricing

Editorial Pricing

Pricing set like a rate card in a magazine's back pages: rules, small caps, tabular numerals, and zero cards.

Workspaceblockpricing

paxfx add blocks/block-pricing-editorial-11local only

Tier: 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>
  );
}