{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pricing-table",
  "title": "Pricing Table",
  "description": "Marketing pricing surface — tier cards with a highlighted popular plan, a monthly/annual billing toggle that swaps per-period prices, and a features-by-plans comparison-table variant with tick/dash cells.",
  "dependencies": [
    "@untitledui/icons@^0.0.22"
  ],
  "registryDependencies": [
    "@nx-ui/badge",
    "@nx-ui/button",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/marketing/pricing-table.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Minus } from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\n\n/**\n * PricingTable — the marketing pricing surface, in two idioms driven by\n * `variant`:\n *\n *   - `cards` (default) — a row of tier cards, each with a name, a per-period\n *     price, a description, a feature checklist (included ticks / excluded\n *     dashes), and a CTA. One tier can be `highlighted` — it gains a brand ring\n *     and a \"popular\" badge and lifts above the rest.\n *   - `comparison` — a features × plans grid, with grouped rows and a\n *     tick/dash (or per-tier value) cell for every plan.\n *\n * Tiers may carry both a `monthlyPrice` and an `annualPrice`; when any does, a\n * monthly/annual billing toggle appears and swaps the prices per period. Prices\n * are either a number (rendered with the `currency` prefix and a `/mo` suffix)\n * or a string (`\"Custom\"`, `\"Free\"`, …) rendered verbatim. Everything is\n * prop-driven — no data coupling.\n */\n\ntype Billing = \"monthly\" | \"annual\"\n\nexport interface PricingFeature {\n  label: React.ReactNode\n  /** `false` renders a muted dash instead of a tick. */\n  included?: boolean\n}\n\nexport interface PricingTier {\n  id: string\n  name: string\n  description?: React.ReactNode\n  /** A number is formatted with `currency` + `/mo`; a string renders verbatim. */\n  monthlyPrice: number | string\n  /** Per-month price when billed annually. Enables the billing toggle. */\n  annualPrice?: number | string\n  /** Overrides the default per-period caption under the price. */\n  period?: string\n  features?: PricingFeature[]\n  cta: { label: string; href: string }\n  /** Renders the accent CTA + copy in place of the plain feature CTA. */\n  ctaVariant?: React.ComponentProps<typeof Button>[\"variant\"]\n  /** The \"popular\" tier — brand ring, badge and a subtle lift. */\n  highlighted?: boolean\n  /** Overrides the default \"Most popular\" badge on the highlighted tier. */\n  badge?: string\n}\n\n/** A single row in the comparison grid. */\nexport interface ComparisonRow {\n  label: React.ReactNode\n  /** One entry per tier, in tier order — `true`/`false` for ticks, or a value. */\n  values: Array<boolean | React.ReactNode>\n}\n\n/** A titled group of comparison rows. */\nexport interface ComparisonGroup {\n  label?: string\n  rows: ComparisonRow[]\n}\n\ninterface PricingTableProps {\n  tiers: PricingTier[]\n  variant?: \"cards\" | \"comparison\"\n  /** Feature matrix for the `comparison` variant (values indexed by tier order). */\n  featureGroups?: ComparisonGroup[]\n  /** Controlled billing period. */\n  billing?: Billing\n  defaultBilling?: Billing\n  onBillingChange?: (billing: Billing) => void\n  /** Caption beside the annual toggle option, e.g. \"Save 20%\". */\n  annualNote?: string\n  currency?: string\n  className?: string\n}\n\nfunction formatPrice(value: number | string, currency: string) {\n  if (typeof value === \"string\") return { amount: value, suffix: undefined }\n  return { amount: `${currency}${value.toLocaleString(\"en-GB\")}`, suffix: \"/mo\" }\n}\n\nfunction BillingToggle({\n  billing,\n  onChange,\n  annualNote,\n}: {\n  billing: Billing\n  onChange: (billing: Billing) => void\n  annualNote?: string\n}) {\n  const options: Array<{ value: Billing; label: string }> = [\n    { value: \"monthly\", label: \"Monthly\" },\n    { value: \"annual\", label: \"Annual\" },\n  ]\n  return (\n    <div\n      role=\"radiogroup\"\n      aria-label=\"Billing period\"\n      className=\"inline-flex items-center gap-1 rounded-full border border-border-secondary bg-bg-secondary p-1\"\n    >\n      {options.map((option) => {\n        const active = billing === option.value\n        return (\n          <button\n            key={option.value}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={active}\n            onClick={() => onChange(option.value)}\n            className={cn(\n              \"inline-flex cursor-pointer items-center gap-2 rounded-full px-4 py-1.5 text-sm font-semibold transition duration-150 ease-linear outline-none focus-visible:ring-4 focus-visible:ring-ring/25\",\n              active\n                ? \"bg-bg-primary text-text-primary shadow-xs\"\n                : \"text-text-tertiary hover:text-text-secondary\"\n            )}\n          >\n            {option.label}\n            {option.value === \"annual\" && annualNote ? (\n              <Badge variant=\"success\">{annualNote}</Badge>\n            ) : null}\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n\nfunction Price({\n  tier,\n  billing,\n  currency,\n}: {\n  tier: PricingTier\n  billing: Billing\n  currency: string\n}) {\n  const raw =\n    billing === \"annual\" && tier.annualPrice != null\n      ? tier.annualPrice\n      : tier.monthlyPrice\n  const { amount, suffix } = formatPrice(raw, currency)\n  const caption =\n    tier.period ??\n    (suffix\n      ? billing === \"annual\"\n        ? \"per month, billed annually\"\n        : \"per month\"\n      : undefined)\n  return (\n    <div className=\"flex flex-col gap-1\">\n      <div className=\"flex items-baseline gap-1\">\n        <span className=\"text-display-sm font-semibold tracking-tight text-text-primary md:text-display-md\">\n          {amount}\n        </span>\n        {suffix ? (\n          <span className=\"text-md font-medium text-text-tertiary\">{suffix}</span>\n        ) : null}\n      </div>\n      {caption ? (\n        <span className=\"text-sm text-text-tertiary\">{caption}</span>\n      ) : null}\n    </div>\n  )\n}\n\nfunction TierCard({\n  tier,\n  billing,\n  currency,\n}: {\n  tier: PricingTier\n  billing: Billing\n  currency: string\n}) {\n  const highlighted = Boolean(tier.highlighted)\n  return (\n    <div\n      data-slot=\"pricing-tier\"\n      data-highlighted={highlighted || undefined}\n      className={cn(\n        \"relative flex flex-col gap-6 rounded-2xl p-7 md:p-8\",\n        highlighted\n          ? \"bg-bg-primary ring-2 ring-brand-500 lg:-my-2 lg:shadow-xl\"\n          : \"border border-border-secondary bg-bg-primary\"\n      )}\n    >\n      <div className=\"flex items-start justify-between gap-3\">\n        <h3 className=\"text-lg font-semibold text-text-primary\">{tier.name}</h3>\n        {highlighted ? (\n          <Badge variant=\"brand\">{tier.badge ?? \"Most popular\"}</Badge>\n        ) : null}\n      </div>\n\n      <Price tier={tier} billing={billing} currency={currency} />\n\n      {tier.description ? (\n        <p className=\"text-md text-text-tertiary\">{tier.description}</p>\n      ) : null}\n\n      <Button\n        variant={tier.ctaVariant ?? (highlighted ? \"default\" : \"secondary\")}\n        size=\"lg\"\n        className=\"w-full\"\n        nativeButton={false}\n        render={<a href={tier.cta.href} />}\n      >\n        {tier.cta.label}\n      </Button>\n\n      {tier.features?.length ? (\n        <ul className=\"flex flex-col gap-3 border-t border-border-secondary pt-6\">\n          {tier.features.map((feature, i) => {\n            const included = feature.included !== false\n            return (\n              <li key={i} className=\"flex items-start gap-3 text-md\">\n                <span\n                  aria-hidden=\"true\"\n                  className={cn(\n                    \"mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full\",\n                    included\n                      ? \"bg-bg-success-secondary text-fg-success-primary\"\n                      : \"bg-bg-tertiary text-fg-quaternary\"\n                  )}\n                >\n                  {included ? (\n                    <Check className=\"size-3.5\" />\n                  ) : (\n                    <Minus className=\"size-3.5\" />\n                  )}\n                </span>\n                <span\n                  className={cn(\n                    included ? \"text-text-secondary\" : \"text-text-quaternary\"\n                  )}\n                >\n                  {feature.label}\n                </span>\n              </li>\n            )\n          })}\n        </ul>\n      ) : null}\n    </div>\n  )\n}\n\nfunction ComparisonCell({ value }: { value: boolean | React.ReactNode }) {\n  if (value === true) {\n    return (\n      <span\n        aria-label=\"Included\"\n        className=\"mx-auto flex size-5 items-center justify-center rounded-full bg-bg-success-secondary text-fg-success-primary\"\n      >\n        <Check className=\"size-3.5\" />\n      </span>\n    )\n  }\n  if (value === false) {\n    return (\n      <span\n        aria-label=\"Not included\"\n        className=\"mx-auto flex size-5 items-center justify-center rounded-full bg-bg-tertiary text-fg-quaternary\"\n      >\n        <Minus className=\"size-3.5\" />\n      </span>\n    )\n  }\n  return (\n    <span className=\"text-sm font-medium text-text-secondary\">{value}</span>\n  )\n}\n\nfunction ComparisonTable({\n  tiers,\n  featureGroups,\n  billing,\n  currency,\n}: {\n  tiers: PricingTier[]\n  featureGroups: ComparisonGroup[]\n  billing: Billing\n  currency: string\n}) {\n  const cols = tiers.length\n  return (\n    <div className=\"overflow-x-auto\">\n      <table className=\"w-full min-w-[720px] border-separate border-spacing-0 text-left\">\n        <thead>\n          <tr>\n            <th className=\"w-[28%] p-4 align-bottom\" scope=\"col\">\n              <span className=\"sr-only\">Feature</span>\n            </th>\n            {tiers.map((tier) => {\n              const raw =\n                billing === \"annual\" && tier.annualPrice != null\n                  ? tier.annualPrice\n                  : tier.monthlyPrice\n              const { amount, suffix } = formatPrice(raw, currency)\n              return (\n                <th\n                  key={tier.id}\n                  scope=\"col\"\n                  className={cn(\n                    \"p-4 align-bottom\",\n                    tier.highlighted && \"rounded-t-2xl bg-bg-secondary\"\n                  )}\n                >\n                  <div className=\"flex flex-col gap-2\">\n                    <span className=\"flex items-center gap-2 text-md font-semibold text-text-primary\">\n                      {tier.name}\n                      {tier.highlighted ? (\n                        <Badge variant=\"brand\">{tier.badge ?? \"Popular\"}</Badge>\n                      ) : null}\n                    </span>\n                    <span className=\"flex items-baseline gap-1\">\n                      <span className=\"text-xl font-semibold text-text-primary\">\n                        {amount}\n                      </span>\n                      {suffix ? (\n                        <span className=\"text-sm text-text-tertiary\">\n                          {suffix}\n                        </span>\n                      ) : null}\n                    </span>\n                    <Button\n                      variant={\n                        tier.ctaVariant ??\n                        (tier.highlighted ? \"default\" : \"secondary\")\n                      }\n                      size=\"sm\"\n                      nativeButton={false}\n                      render={<a href={tier.cta.href} />}\n                    >\n                      {tier.cta.label}\n                    </Button>\n                  </div>\n                </th>\n              )\n            })}\n          </tr>\n        </thead>\n        <tbody>\n          {featureGroups.map((group, gi) => (\n            <React.Fragment key={gi}>\n              {group.label ? (\n                <tr>\n                  <th\n                    scope=\"colgroup\"\n                    colSpan={cols + 1}\n                    className=\"px-4 pt-8 pb-2 text-sm font-semibold tracking-wide text-text-brand-secondary uppercase\"\n                  >\n                    {group.label}\n                  </th>\n                </tr>\n              ) : null}\n              {group.rows.map((row, ri) => (\n                <tr key={ri} className=\"group/row\">\n                  <th\n                    scope=\"row\"\n                    className=\"border-t border-border-secondary p-4 text-sm font-medium text-text-secondary\"\n                  >\n                    {row.label}\n                  </th>\n                  {tiers.map((tier, ti) => (\n                    <td\n                      key={tier.id}\n                      className={cn(\n                        \"border-t border-border-secondary p-4 text-center\",\n                        tier.highlighted && \"bg-bg-secondary\"\n                      )}\n                    >\n                      <ComparisonCell value={row.values[ti]} />\n                    </td>\n                  ))}\n                </tr>\n              ))}\n            </React.Fragment>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  )\n}\n\nfunction PricingTable({\n  tiers,\n  variant = \"cards\",\n  featureGroups,\n  billing: billingProp,\n  defaultBilling = \"monthly\",\n  onBillingChange,\n  annualNote,\n  currency = \"£\",\n  className,\n}: PricingTableProps) {\n  const [internal, setInternal] = React.useState<Billing>(defaultBilling)\n  const billing = billingProp ?? internal\n  const setBilling = React.useCallback(\n    (next: Billing) => {\n      if (billingProp == null) setInternal(next)\n      onBillingChange?.(next)\n    },\n    [billingProp, onBillingChange]\n  )\n\n  const hasAnnual = tiers.some((tier) => tier.annualPrice != null)\n\n  return (\n    <div data-slot=\"pricing-table\" className={cn(\"flex flex-col gap-10\", className)}>\n      {hasAnnual ? (\n        <div className=\"flex justify-center\">\n          <BillingToggle\n            billing={billing}\n            onChange={setBilling}\n            annualNote={annualNote}\n          />\n        </div>\n      ) : null}\n\n      {variant === \"comparison\" && featureGroups ? (\n        <ComparisonTable\n          tiers={tiers}\n          featureGroups={featureGroups}\n          billing={billing}\n          currency={currency}\n        />\n      ) : (\n        <div\n          className={cn(\n            \"grid items-start gap-6\",\n            tiers.length >= 4\n              ? \"sm:grid-cols-2 xl:grid-cols-4\"\n              : tiers.length === 3\n                ? \"sm:grid-cols-2 lg:grid-cols-3\"\n                : \"sm:grid-cols-2\"\n          )}\n        >\n          {tiers.map((tier) => (\n            <TierCard\n              key={tier.id}\n              tier={tier}\n              billing={billing}\n              currency={currency}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport { PricingTable }\n",
      "type": "registry:component",
      "target": "components/blocks/marketing/pricing-table.tsx"
    }
  ],
  "type": "registry:block"
}