{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pricing-3",
  "title": "Pricing 3: full plan comparison",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A feature-by-feature comparison table with a sticky plan header, collapsible groups and a highlighted column. On phones it becomes a plan switcher with one clean list.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/badge.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/segmented-control.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/pricing-3/pricing-3.tsx",
      "content": "// Ballmac UI: Pricing 3. https://ui.ballmac.com/blocks/pricing-3\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronDown, Minus } from \"lucide-react\"\n\nimport { Badge } from \"@/components/ballmac/badge\"\nimport { buttonVariants } from \"@/components/ballmac/button\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { cn } from \"@/lib/utils\"\n\ntype Pricing3Plan = {\n  /** Stable key used by the feature rows. */\n  key: string\n  /** Plan name. */\n  name: string\n  /** Price text, e.g. \"$29\". */\n  price: string\n  /** What the price covers, e.g. \"per seat / month\". */\n  period?: string\n  /** Button. */\n  cta: { label: string; href: string }\n  /** Emphasise this plan. */\n  featured?: boolean\n}\n\ntype Pricing3Row = {\n  /** Feature name. */\n  label: string\n  /** Extra detail shown under the name. */\n  hint?: string\n  /** Value for each plan key: true (included), false (not included) or text such as \"10 GB\". */\n  values: Record<string, boolean | string>\n}\n\ntype Pricing3Group = {\n  /** Group heading, e.g. \"Collaboration\". */\n  title: string\n  rows: Pricing3Row[]\n}\n\ntype Pricing3Props = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  /** Section heading. */\n  title?: string\n  /** One sentence under the heading. */\n  description?: string\n  /** The columns to compare. */\n  plans?: Pricing3Plan[]\n  /** Feature groups, each a set of rows. */\n  groups?: Pricing3Group[]\n  /** Distance in pixels the sticky header keeps from the top of the page, e.g. the height of your site header. */\n  stickyOffset?: number\n}\n\nconst defaultPlans: Pricing3Plan[] = [\n  { key: \"starter\", name: \"Starter\", price: \"$12\", period: \"per month\", cta: { label: \"Start free\", href: \"#\" } },\n  { key: \"pro\", name: \"Pro\", price: \"$32\", period: \"per seat / month\", cta: { label: \"Start trial\", href: \"#\" }, featured: true },\n  { key: \"scale\", name: \"Scale\", price: \"$79\", period: \"per seat / month\", cta: { label: \"Talk to sales\", href: \"#\" } },\n]\n\nconst defaultGroups: Pricing3Group[] = [\n  {\n    title: \"Core\",\n    rows: [\n      { label: \"Projects\", values: { starter: \"3\", pro: \"Unlimited\", scale: \"Unlimited\" } },\n      { label: \"Storage\", values: { starter: \"10 GB\", pro: \"1 TB\", scale: \"10 TB\" } },\n      { label: \"Version history\", hint: \"Restore any earlier version\", values: { starter: \"7 days\", pro: \"90 days\", scale: \"Unlimited\" } },\n      { label: \"Custom domain\", values: { starter: false, pro: true, scale: true } },\n    ],\n  },\n  {\n    title: \"Collaboration\",\n    rows: [\n      { label: \"Guests\", values: { starter: \"5\", pro: \"50\", scale: \"Unlimited\" } },\n      { label: \"Comments and mentions\", values: { starter: true, pro: true, scale: true } },\n      { label: \"Team roles\", values: { starter: false, pro: true, scale: true } },\n      { label: \"Shared workspaces\", values: { starter: false, pro: true, scale: true } },\n    ],\n  },\n  {\n    title: \"Security and support\",\n    rows: [\n      { label: \"Single sign-on (SAML)\", values: { starter: false, pro: true, scale: true } },\n      { label: \"Audit log\", values: { starter: false, pro: false, scale: true } },\n      { label: \"SCIM provisioning\", values: { starter: false, pro: false, scale: true } },\n      { label: \"Support\", values: { starter: \"Community\", pro: \"Priority email\", scale: \"Dedicated manager\" } },\n      { label: \"Uptime SLA\", values: { starter: false, pro: false, scale: \"99.99%\" } },\n    ],\n  },\n]\n\nfunction Value({ value, label }: { value: boolean | string; label: string }) {\n  if (typeof value === \"string\") return <span className=\"text-sm font-medium\">{value}</span>\n  return value ? (\n    <Check className=\"text-chart-2 mx-auto size-4.5\" aria-label={`${label}: included`} role=\"img\" />\n  ) : (\n    <Minus className=\"text-muted-foreground mx-auto size-4\" aria-label={`${label}: not included`} role=\"img\" />\n  )\n}\n\nfunction Pricing3({\n  title = \"Compare every plan.\",\n  description = \"All the details, side by side.\",\n  plans = defaultPlans,\n  groups = defaultGroups,\n  stickyOffset = 0,\n  className,\n  ...props\n}: Pricing3Props) {\n  const [closed, setClosed] = React.useState<Set<string>>(() => new Set())\n  const [mobilePlan, setMobilePlan] = React.useState(plans.find((p) => p.featured)?.key ?? plans[0]?.key ?? \"\")\n  const selected = plans.find((p) => p.key === mobilePlan) ?? plans[0]\n  const baseId = React.useId()\n\n  function toggle(group: string) {\n    setClosed((prev) => {\n      const next = new Set(prev)\n      if (next.has(group)) next.delete(group)\n      else next.add(group)\n      return next\n    })\n  }\n\n  return (\n    <section data-slot=\"pricing-3\" className={cn(\"mx-auto max-w-5xl px-4 py-20 sm:px-6 md:py-28\", className)} {...props}>\n      <div className=\"mx-auto max-w-2xl text-center\">\n        <h2 className=\"text-3xl font-semibold tracking-[-0.035em] text-balance sm:text-4xl lg:text-5xl\">{title}</h2>\n        <p className=\"text-muted-foreground mt-4 text-lg text-pretty\">{description}</p>\n      </div>\n\n      {/* Phones: one plan at a time. */}\n      {selected && (\n        <div className=\"mt-10 md:hidden\">\n          <SegmentedControl aria-label=\"Plan to view\" value={selected.key} onValueChange={setMobilePlan} fullWidth>\n            {plans.map((p) => (\n              <SegmentedControlItem key={p.key} value={p.key}>{p.name}</SegmentedControlItem>\n            ))}\n          </SegmentedControl>\n          <div className=\"bg-card mt-5 rounded-2xl border p-5\">\n            <div className=\"flex items-baseline gap-2\">\n              <span className=\"text-4xl font-semibold tracking-[-0.04em]\">{selected.price}</span>\n              {selected.period && <span className=\"text-muted-foreground text-sm\">{selected.period}</span>}\n            </div>\n            <a href={selected.cta.href} className={buttonVariants({ variant: selected.featured ? \"default\" : \"outline\", shape: \"pill\", className: \"mt-4 w-full\" })}>\n              {selected.cta.label}\n            </a>\n          </div>\n          {groups.map((g) => (\n            <div key={g.title} className=\"mt-6\">\n              <h3 className=\"text-muted-foreground px-1 pb-2 text-xs font-semibold tracking-wide uppercase\">{g.title}</h3>\n              <ul className=\"bg-card divide-y rounded-2xl border\">\n                {g.rows.map((r) => (\n                  <li key={r.label} className=\"flex items-center justify-between gap-4 px-4 py-3\">\n                    <span className=\"text-sm\">{r.label}</span>\n                    <span className=\"shrink-0 text-end\"><Value value={r.values[selected.key] ?? false} label={r.label} /></span>\n                  </li>\n                ))}\n              </ul>\n            </div>\n          ))}\n        </div>\n      )}\n\n      {/* Tablets and up: the full table with a sticky header. */}\n      <div className=\"mt-14 hidden md:block\">\n        <table className=\"w-full border-separate border-spacing-0 text-start\">\n          <caption className=\"sr-only\">Plan comparison</caption>\n          <thead>\n            <tr>\n              <th scope=\"col\" className=\"bg-background/90 sticky z-10 w-[32%] border-b p-0 backdrop-blur-xl\" style={{ top: stickyOffset }}>\n                <span className=\"sr-only\">Feature</span>\n              </th>\n              {plans.map((p) => (\n                <th\n                  key={p.key}\n                  scope=\"col\"\n                  style={{ top: stickyOffset }}\n                  className={cn(\"bg-background/90 sticky z-10 border-b px-4 pt-2 pb-5 align-bottom backdrop-blur-xl\", p.featured && \"bg-muted/60\")}\n                >\n                  <span className=\"flex items-center gap-2 text-base font-semibold\">\n                    {p.name}\n                    {p.featured && <Badge>Popular</Badge>}\n                  </span>\n                  <span className=\"mt-2 flex items-baseline gap-1.5 font-normal\">\n                    <span className=\"text-3xl font-semibold tracking-[-0.04em]\">{p.price}</span>\n                    {p.period && <span className=\"text-muted-foreground text-xs\">{p.period}</span>}\n                  </span>\n                  <a href={p.cta.href} className={buttonVariants({ variant: p.featured ? \"default\" : \"outline\", size: \"sm\", shape: \"pill\", className: \"mt-4 w-full\" })}>\n                    {p.cta.label}\n                  </a>\n                </th>\n              ))}\n            </tr>\n          </thead>\n          {groups.map((g, gi) => {\n            const open = !closed.has(g.title)\n            const panelId = `${baseId}-g${gi}`\n            return (\n              <tbody key={g.title}>\n                <tr>\n                  <th scope=\"colgroup\" colSpan={plans.length + 1} className=\"p-0 pt-8 text-start\">\n                    <button\n                      type=\"button\"\n                      aria-expanded={open}\n                      aria-controls={panelId}\n                      onClick={() => toggle(g.title)}\n                      className=\"focus-visible:ring-ring/50 group/head flex w-full items-center justify-between rounded-lg px-1 py-2 text-sm font-semibold outline-none focus-visible:ring-[3px]\"\n                    >\n                      {g.title}\n                      <ChevronDown className={cn(\"text-muted-foreground size-4 transition-transform duration-200 motion-reduce:transition-none\", !open && \"-rotate-90\")} aria-hidden=\"true\" />\n                    </button>\n                  </th>\n                </tr>\n                {open &&\n                  g.rows.map((r, ri) => (\n                    <tr key={r.label} id={ri === 0 ? panelId : undefined} className=\"group/row\">\n                      <th scope=\"row\" className=\"border-t px-1 py-3.5 text-start text-sm font-normal\">\n                        {r.label}\n                        {r.hint && <span className=\"text-muted-foreground block text-xs\">{r.hint}</span>}\n                      </th>\n                      {plans.map((p) => (\n                        <td key={p.key} className={cn(\"border-t px-4 py-3.5 text-center\", p.featured && \"bg-muted/40\")}>\n                          <Value value={r.values[p.key] ?? false} label={r.label} />\n                        </td>\n                      ))}\n                    </tr>\n                  ))}\n              </tbody>\n            )\n          })}\n        </table>\n      </div>\n    </section>\n  )\n}\n\nexport { Pricing3, type Pricing3Props, type Pricing3Plan, type Pricing3Group, type Pricing3Row }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/pricing-3/pricing-3.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "pricing",
      "comparison",
      "table",
      "sticky",
      "plans",
      "features"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "The detailed pricing table for buyers who compare. Pass plans=[{ key, name, price, period?, cta, featured? }] and groups=[{ title, rows: [{ label, hint?, values: { [planKey]: boolean | string } }] }].",
      "whenToUse": [
        "Below pricing cards, for buyers who want every detail",
        "Products with many differentiating features"
      ],
      "whenNotToUse": [
        "Two simple plans (pricing-1 is enough)"
      ],
      "composesWith": [
        "pricing-2",
        "faq-2",
        "cta-1",
        "header-1"
      ],
      "a11y": [
        {
          "keys": "Enter / Space on a group heading",
          "action": "Collapses or expands the group"
        },
        {
          "keys": "Arrow Left / Right (phones)",
          "action": "Switches the plan shown"
        }
      ],
      "customization": [
        "stickyOffset: pixels to keep from the top when you have a fixed site header",
        "values: true / false render an icon with a text label; strings render as text"
      ]
    },
    "examples": [
      "pricing-3-demo",
      "pricing-3-two"
    ],
    "url": "https://ui.ballmac.com/components/pricing-3"
  },
  "categories": [
    "blocks",
    "pricing"
  ],
  "type": "registry:block"
}