{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "plan-fit",
  "type": "registry:ui",
  "title": "Plan Fit",
  "description": "A pricing grid with a usage slider over it, cut into one stretch per plan. Drag it and every card measures your usage against its allowance: past the limit the overage shows as a hatched stretch and that card's price climbs a pack at a time. The highlight glides to whichever plan is cheapest at that usage, so a smaller plan with overage can keep it until the next one costs less.",
  "author": "KeyframeUI <https://keyframeui.com>",
  "categories": [
    "pricing",
    "interaction",
    "data"
  ],
  "docs": "Live preview, examples and props: https://keyframeui.com/components/plan-fit",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/plan-fit.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { animate, motion, useMotionValue, useMotionValueEvent, useTransform, type MotionValue } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Keep:\n * - Usage, prices, meters and the readout are motion values derived from one `pos`, written into\n *   text nodes, `transform`s and the slider's ARIA values, so dragging never re-renders a card.\n * - The highlight is one layer under the cards that glides by `transform` and turns from wherever\n *   it is; a card's accents arrive only as it lands and leave first, so two cards are never lit.\n * - Pressing the ball never moves it: the drag keeps the offset it was taken hold at. A finger\n *   drags only once it moves sideways; a vertical move scrolls the page.\n * - Reduced motion re-keys the highlight so it fades in at its new card; jumps, prices and the\n *   swipe row land at once, and dragging still moves everything 1:1.\n * - The thumb is the `role=\"slider\"` keyboard target, and a new fit is announced through the\n *   polite live region.\n */\nconst TIMING = {\n  /**\n   * The highlight, its ring and the tag gliding to a new best plan: a spring, in seconds\n   * (`visualDuration` is the `glideDuration` prop, `bounce` the `glideBounce` prop).\n   */\n  glide: { type: \"spring\", visualDuration: 0.42, bounce: 0.12 },\n  /** The thumb carried to a plan's allowance, in seconds, on a strong ease-out. */\n  jump: { duration: 0.45, ease: [0.23, 1, 0.32, 1] },\n  /** The thumb gliding to a click on the rail, in seconds. */\n  press: { duration: 0.22, ease: [0.23, 1, 0.32, 1] },\n  /** Prices counting to a new billing option, in seconds (`duration` is the `countDuration` prop). */\n  count: { duration: 0.35, ease: [0.23, 1, 0.32, 1] },\n  /** The billing switch's pill: a spring, in seconds. */\n  slide: { type: \"spring\", visualDuration: 0.3, bounce: 0 },\n  /** The highlight appearing in place, under reduced motion, in seconds. */\n  fade: { duration: 0.18, ease: \"easeOut\" },\n  /** A billing option's text color, as a Tailwind class. */\n  option: \"duration-150\",\n  /** The ball growing while held, and its focus halo, as Tailwind classes. */\n  ball: \"duration-150 ease-out\",\n  /** A focus this long after a press is the keyboard's, and shows the halo, in ms. */\n  pressFocus: 300,\n  /** A card fading to 45% when its plan is outgrown, as a Tailwind class. */\n  outgrow: \"duration-200\",\n  /**\n   * A card's accents, keyed off its `data-fit`, as Tailwind classes. Accents leave a card in 0.1s\n   * and arrive in 0.2s after 0.12s, as the highlight lands, so two cards are never lit at once.\n   */\n  accent:\n    \"duration-100 ease-out group-data-[fit=true]/card:delay-[120ms] group-data-[fit=true]/card:duration-200\",\n  /** The curve of the rail names' and the tag's accents, as a Tailwind class. */\n  accentEase: \"ease-out\",\n  /** Those accents arriving at the fit, as Tailwind classes. */\n  accentIn: \"delay-[120ms] duration-200\",\n  /** And leaving the last fit, as a Tailwind class. */\n  accentOut: \"duration-100\",\n  /** The fit's button as a CSS transition: its fill on the accent timing, hover and press at once. */\n  ctaIn:\n    \"[transition:background-color_200ms_ease-out_120ms,color_200ms_ease-out_120ms,scale_150ms_ease-out,filter_150ms_ease-out]\",\n  /** Every other plan's button, as a CSS transition class. */\n  ctaOut: \"[transition:background-color_100ms_ease-out,color_100ms_ease-out,scale_150ms_ease-out]\",\n} as const;\n\n/* ─────────────────────────────────────────────────────────\n * PLAN FIT STORYBOARD   (driven by the visitor's usage, not by time)\n *\n *  rest      a usage readout over a rail cut into one stretch per\n *            plan, and the plan cards under it, each card under its\n *            own stretch. A tick marks where each plan's allowance\n *            ends. The cheapest plan for the starting usage already\n *            wears the highlight and its \"Best fit\" tag\n *  drag      the thumb follows the pointer 1:1. Pressing the ball\n *            itself never moves it: the drag keeps the spot you took\n *            hold of, so the ball stays under your finger. Usage climbs on a\n *            log scale inside each stretch, rounded to two figures,\n *            and the readout, every meter and every price follow on\n *            the same frame. Nothing re-renders while it moves\n *  meter     each card measures the usage against its allowance and\n *            fills toward its limit tick. Past it the bar rescales:\n *            the tick slides left to where the allowance ends inside\n *            the usage, the hatched stretch after it is overage, and\n *            the price climbs a pack at a time\n *  outgrow   a plan with no overage stops at its allowance. Past it\n *            the card fades to 45% in 0.2s and its meter says so\n *  glide     when another plan becomes the cheapest, the highlight,\n *            its ring and the tag glide over to it (spring 0.42s,\n *            bounce 0.12) and turn from wherever they are if it moves\n *            again. The plan's rail label and button take the accent\n *            in 0.15s. A smaller plan with overage can stay the fit\n *            well past its tick, until the next one is cheaper\n *  jump      a plan's name under the rail, Shift+arrows and\n *            PageUp/PageDown carry the thumb to an allowance in 0.45s\n *            on a strong ease-out, and everything sweeps along.\n *            A click on the rail glides there in 0.22s\n *  billing   another billing option counts every price to its new\n *            figure in 0.35s; the fit may move with it\n *  narrow    below the width where the cards fit in a row they become\n *            a swipe row, and each new fit scrolls to the middle\n *  smooth    the thumb, the fill and the meters move by transform,\n *            the highlight is one layer gliding on the compositor,\n *            and numbers are written straight into their text nodes\n *  touch     a horizontal drag moves the thumb, a vertical one scrolls\n *            the page, a tap glides to the spot\n *  keys      the thumb is a slider: arrows step 1% of the rail,\n *            Shift+arrows and PageUp/PageDown go plan to plan, Home\n *            and End go to the ends. A new fit is announced politely\n *  reduced   nothing glides, sweeps or counts: the highlight fades in\n *            at its new card in 0.18s, jumps and prices land at once\n *            and the swipe row snaps. Dragging still moves it all 1:1\n * ───────────────────────────────────────────────────────── */\n\nconst FIT = {\n  step: 0.01, // share of the rail an arrow key moves the thumb\n  grab: 16, // px from the ball's center that count as taking hold of it, not pressing the rail\n  slop: 6, // px a finger travels before it counts as a sideways drag rather than a scroll\n  figures: 2, // significant figures usage is rounded to\n  start: 0.6, // how far along the second plan's stretch the thumb starts\n} as const;\n\n// From these container widths the cards sit in one row, each under its own\n// stretch of the rail, and the plan names under the rail give way to the cards\n// themselves. Narrower, the cards become a swipe row of fixed-width cards.\nconst ROW: Record<number, { root: string; track: string; names: string }> = {\n  1: { root: \"[--plan-fit-card:100%]\", track: \"w-full\", names: \"hidden\" },\n  2: {\n    root: \"@[30rem]/plan-fit:[--plan-fit-card:50%]\",\n    track: \"@[30rem]/plan-fit:w-full\",\n    names: \"@[30rem]/plan-fit:hidden\",\n  },\n  3: {\n    root: \"@[40rem]/plan-fit:[--plan-fit-card:33.3333%]\",\n    track: \"@[40rem]/plan-fit:w-full\",\n    names: \"@[40rem]/plan-fit:hidden\",\n  },\n  4: {\n    root: \"@[48rem]/plan-fit:[--plan-fit-card:25%]\",\n    track: \"@[48rem]/plan-fit:w-full\",\n    names: \"@[48rem]/plan-fit:hidden\",\n  },\n  5: {\n    root: \"@[60rem]/plan-fit:[--plan-fit-card:20%]\",\n    track: \"@[60rem]/plan-fit:w-full\",\n    names: \"@[60rem]/plan-fit:hidden\",\n  },\n};\n\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\nconst formats = new Map<string, Intl.NumberFormat>();\nfunction format(locale: string, options: Intl.NumberFormatOptions) {\n  const key = locale + JSON.stringify(options);\n  let formatter = formats.get(key);\n  if (!formatter) {\n    formatter = new Intl.NumberFormat(locale, options);\n    formats.set(key, formatter);\n  }\n  return formatter;\n}\n\n/* ── types ──────────────────────────────────────────────── */\n\n/** Extra usage past a plan's allowance, sold in packs. */\nexport interface PlanFitOverage {\n  /** What one pack costs, in `currency`. */\n  price: number;\n  /** How much usage one pack adds. */\n  per: number;\n}\n\n/** What a plan's button does: a link with `href`, or a button with `onClick`. */\nexport interface PlanFitCta {\n  label: string;\n  href?: string;\n  onClick?: () => void;\n}\n\nexport type PlanFitPrice = number | string;\n\nexport interface PlanFitPlan {\n  id: string;\n  name: string;\n  /** One or two lines under the name. */\n  pitch?: React.ReactNode;\n  /**\n   * The monthly price. Give one per billing option as `{ monthly: 29, yearly: 23 }`.\n   * Text (\"Custom\") makes it a quoted plan: it only becomes the fit when no priced plan can take the usage.\n   */\n  price: PlanFitPrice | Record<string, PlanFitPrice>;\n  /** The usage the price covers each month. Leave it out on a quoted plan that takes any amount. */\n  included?: number;\n  /** Extra usage past `included`, sold in packs: `{ price: 10, per: 1000 }`. Without it the plan stops at `included`. */\n  overage?: PlanFitOverage;\n  /**\n   * The most usage the plan takes, overage included; past it the plan is outgrown. Defaults to `included`, or with\n   * `overage` to the next plan's `included` (no cap on the last plan).\n   */\n  limit?: number;\n  /** Small print under the price, one for all billing options or one per option. Overage replaces it while there is some. */\n  caption?: React.ReactNode | Record<string, React.ReactNode>;\n  /** What the plan includes, under the button. */\n  features?: React.ReactNode[];\n  cta?: PlanFitCta;\n}\n\nexport interface PlanFitBilling {\n  /** Keys each plan's per-option `price` and `caption`. */\n  id: string;\n  label: string;\n  /** A short accent note after the label, e.g. \"−20%\". */\n  note?: React.ReactNode;\n}\n\nexport interface PlanFitProps extends Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"defaultValue\" | \"onChange\" | \"children\"\n> {\n  /** The plans, smallest allowance first. Each gets a card and a stretch of the rail; up to five fit in a row. */\n  plans: PlanFitPlan[];\n  /** What usage is counted in, plural: \"credits\", \"minutes\". Used in the readout, the captions and the slider's name. */\n  unit?: string;\n  /** The line under the big number. Defaults to `<unit> per month`. */\n  readoutLabel?: React.ReactNode;\n  /** The usage the thumb starts at. Defaults to a point 60% along the second plan's stretch. */\n  defaultValue?: number;\n  /** The usage at the rail's left end. */\n  min?: number;\n  /** The usage at the rail's right end. Defaults to the last plan's allowance, or four times the largest one when the last plan is quoted. */\n  max?: number;\n  /** Options for a billing switch beside the readout, as `{ id, label, note? }`. Leave it out for one price per plan. */\n  billing?: PlanFitBilling[];\n  /** The billing option selected at first. Defaults to the first. */\n  defaultBilling?: string;\n  /** An ISO currency code for every price. */\n  currency?: string;\n  /** The locale numbers and prices are formatted in. */\n  locale?: string;\n  /** Shown after each price. */\n  period?: string;\n  /** The tag on the highlighted plan, also read out to screen readers. */\n  fitLabel?: string;\n  /** Called with the usage whenever it changes. */\n  onValueChange?: (usage: number) => void;\n  /** Called with the best plan's id once on mount, then whenever a different plan becomes the best fit. */\n  onFitChange?: (planId: string) => void;\n  /** Seconds the highlight takes to glide over to a new best plan (the spring's visual duration). */\n  glideDuration?: number;\n  /** How much the highlight overshoots as it lands on a new plan, from 0 (none) to 1. */\n  glideBounce?: number;\n  /** Seconds every price takes to count to its figure for another billing option. */\n  countDuration?: number;\n}\n\n/* ── the scale: rail position ⇄ usage ───────────────────── */\n\n// The rail is cut into one equal stretch per plan, from the previous plan's\n// allowance to this one's. Inside a stretch usage moves on a log scale (linear\n// when it starts at 0), so 10 and 10,000 are both a short drag away.\nfunction createScale(plans: PlanFitPlan[], min: number, max: number) {\n  const n = plans.length;\n  const bounds = [min];\n  for (let i = 0; i < n - 1; i++) bounds.push(Math.max(bounds[i], plans[i].included ?? bounds[i]));\n  bounds.push(Math.max(bounds[n - 1], max));\n\n  const along = (lo: number, hi: number, f: number) => (lo > 0 && hi > lo ? lo * (hi / lo) ** f : lo + (hi - lo) * f);\n  const back = (lo: number, hi: number, usage: number) =>\n    hi <= lo ? 0 : lo > 0 ? Math.log(usage / lo) / Math.log(hi / lo) : (usage - lo) / (hi - lo);\n\n  // Two significant figures reads 27,000, not 27,384. Allowances snap exactly,\n  // so a thumb on a tick is on the allowance and never a step over it.\n  const round = (raw: number) => {\n    const size = Math.max(1, 10 ** (Math.floor(Math.log10(Math.max(raw, 1))) - FIT.figures + 1));\n    const exact = bounds.find((bound) => Math.abs(bound - raw) <= size / 2);\n    return exact ?? Math.round(raw / size) * size;\n  };\n\n  return {\n    bounds,\n    toUsage(p: number) {\n      const s = clamp(p, 0, 1) * n;\n      const i = Math.min(n - 1, Math.floor(s));\n      return clamp(round(along(bounds[i], bounds[i + 1], s - i)), min, bounds[n]);\n    },\n    toPos(usage: number) {\n      const u = clamp(usage, min, bounds[n]);\n      const i = Math.max(0, bounds.findIndex((bound, k) => k > 0 && u <= bound) - 1);\n      return (i + clamp(back(bounds[i], bounds[i + 1], u), 0, 1)) / n;\n    },\n  };\n}\n\n/* ── pricing ────────────────────────────────────────────── */\n\nconst isPerOption = <T,>(value: unknown): value is Record<string, T> =>\n  !!value && typeof value === \"object\" && !Array.isArray(value) && !React.isValidElement(value);\n\nfunction priceFor(plan: PlanFitPlan, billing?: string): PlanFitPrice {\n  if (!isPerOption<PlanFitPrice>(plan.price)) return plan.price;\n  return (billing !== undefined ? plan.price[billing] : undefined) ?? Object.values(plan.price)[0];\n}\n\n/** The price between two billing options while the switch counts from one to the other. */\nfunction priceAt(plan: PlanFitPlan, billing: PlanFitBilling[] | undefined, at: number): PlanFitPrice {\n  if (!billing?.length) return priceFor(plan);\n  const a = clamp(Math.floor(at), 0, billing.length - 1);\n  const b = Math.min(billing.length - 1, a + 1);\n  const from = priceFor(plan, billing[a].id);\n  const to = priceFor(plan, billing[b].id);\n  if (typeof from !== \"number\" || typeof to !== \"number\") return at - a < 0.5 ? from : to;\n  return from + (to - from) * (at - a);\n}\n\ntype Reading =\n  | { kind: \"quote\" }\n  | { kind: \"outgrown\"; limit: number }\n  | { kind: \"within\"; usage: number; included: number; total: number }\n  | {\n      kind: \"over\";\n      usage: number;\n      included: number;\n      over: number;\n      extra: number;\n      total: number;\n    };\n\n/** The most a plan takes: its own `limit`, else its allowance, else (with overage) up to the next plan's allowance. */\nfunction limitOf(plans: PlanFitPlan[], i: number) {\n  const plan = plans[i];\n  if (plan.limit !== undefined) return plan.limit;\n  if (plan.included === undefined) return Infinity;\n  if (!plan.overage) return plan.included;\n  return Math.max(plan.included, plans[i + 1]?.included ?? Infinity);\n}\n\n/** How a plan copes with a usage, at a base price. */\nfunction read(plan: PlanFitPlan, base: PlanFitPrice, usage: number, limit: number): Reading {\n  if (typeof base !== \"number\" || plan.included === undefined) return { kind: \"quote\" };\n  const included = plan.included;\n  if (usage > limit || (usage > included && !plan.overage)) return { kind: \"outgrown\", limit };\n  if (usage <= included || !plan.overage) return { kind: \"within\", usage, included, total: base };\n  const packs = Math.ceil((usage - included) / plan.overage.per);\n  const extra = packs * plan.overage.price;\n  return {\n    kind: \"over\",\n    usage,\n    included,\n    over: packs * plan.overage.per,\n    extra,\n    total: base + extra,\n  };\n}\n\n/** The cheapest plan that takes the usage; on a tie, the bigger one. Quoted plans step in when no priced plan can. */\nfunction fitFor(plans: PlanFitPlan[], usage: number, billing?: string) {\n  let best = -1;\n  let cheapest = Infinity;\n  plans.forEach((plan, i) => {\n    const reading = read(plan, priceFor(plan, billing), usage, limitOf(plans, i));\n    if ((reading.kind === \"within\" || reading.kind === \"over\") && reading.total <= cheapest) {\n      best = i;\n      cheapest = reading.total;\n    }\n  });\n  if (best >= 0) return best;\n  const quoted = plans.findIndex(\n    (plan, i) => typeof priceFor(plan, billing) !== \"number\" && usage <= limitOf(plans, i),\n  );\n  return quoted >= 0 ? quoted : plans.length - 1;\n}\n\nconst hasCents = (plan: PlanFitPlan) =>\n  [\n    ...(isPerOption<PlanFitPrice>(plan.price) ? Object.values(plan.price) : [plan.price]),\n    plan.overage?.price ?? 0,\n  ].some((price) => typeof price === \"number\" && !Number.isInteger(price));\n\n/* ── reduced motion ─────────────────────────────────────── */\n\n// Read straight from the media query rather than through motion's hook, so the\n// server and the first client render agree and dev builds don't log a warning.\nconst REDUCED = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReduced = (onChange: () => void) => {\n  const query = window.matchMedia(REDUCED);\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n};\nfunction useReduced() {\n  return React.useSyncExternalStore(\n    subscribeReduced,\n    () => window.matchMedia(REDUCED).matches,\n    () => false,\n  );\n}\n\n/* ── BillingSwitch ──────────────────────────────────────── */\n\n// A two-or-more way switch; the pill glides under the chosen option.\nfunction BillingSwitch({\n  options,\n  index,\n  reduced,\n  onSelect,\n}: {\n  options: PlanFitBilling[];\n  index: number;\n  reduced: boolean;\n  onSelect: (index: number) => void;\n}) {\n  const buttons = React.useRef<(HTMLButtonElement | null)[]>([]);\n  const onKeyDown = (event: React.KeyboardEvent) => {\n    const step =\n      event.key === \"ArrowRight\" || event.key === \"ArrowDown\"\n        ? 1\n        : event.key === \"ArrowLeft\" || event.key === \"ArrowUp\"\n          ? -1\n          : 0;\n    if (!step) return;\n    event.preventDefault();\n    const next = (index + step + options.length) % options.length;\n    onSelect(next);\n    buttons.current[next]?.focus();\n  };\n\n  return (\n    <div\n      role=\"radiogroup\"\n      aria-label=\"Billing\"\n      onKeyDown={onKeyDown}\n      className=\"relative grid shrink-0 auto-cols-fr grid-flow-col rounded-full bg-zinc-950/[0.05] p-1 text-[13px] font-medium dark:bg-white/[0.07]\"\n    >\n      <motion.span\n        aria-hidden\n        initial={false}\n        animate={{ transform: `translateX(${index * 100}%)` }}\n        transition={reduced ? { duration: 0 } : TIMING.slide}\n        style={{ width: `calc((100% - 0.5rem) / ${options.length})` }}\n        className=\"absolute inset-y-1 left-1 rounded-full bg-white shadow-[0_1px_2px_rgb(0_0_0/0.08),0_0_0_1px_rgb(0_0_0/0.04)] dark:bg-zinc-700 dark:shadow-none\"\n      />\n      {options.map((option, i) => (\n        <button\n          key={option.id}\n          ref={(node) => {\n            buttons.current[i] = node;\n          }}\n          type=\"button\"\n          role=\"radio\"\n          aria-checked={i === index}\n          tabIndex={i === index ? 0 : -1}\n          onClick={() => onSelect(i)}\n          className={cn(\n            `relative flex items-center justify-center gap-1.5 rounded-full px-3.5 py-1.5 whitespace-nowrap transition-colors ${TIMING.option} outline-none focus-visible:ring-2 focus-visible:ring-(--plan-fit-accent)`,\n            i === index\n              ? \"text-zinc-950 dark:text-white\"\n              : \"text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200\",\n          )}\n        >\n          {option.label}\n          {option.note && <span className=\"text-xs font-semibold text-(--plan-fit-accent-text)\">{option.note}</span>}\n        </button>\n      ))}\n    </div>\n  );\n}\n\n/* ── Rail ───────────────────────────────────────────────── */\n\n// The slider. The thumb is the keyboard target; the whole 44px strip takes\n// the pointer. Position is a motion value from 0 to 1, usage is derived from it.\nfunction Rail({\n  plans,\n  scale,\n  pos,\n  usage,\n  fit,\n  unit,\n  locale,\n  reduced,\n  fitLabel,\n  billingId,\n  namesClassName,\n}: {\n  plans: PlanFitPlan[];\n  scale: ReturnType<typeof createScale>;\n  pos: MotionValue<number>;\n  usage: MotionValue<number>;\n  fit: number;\n  unit: string;\n  locale: string;\n  reduced: boolean;\n  fitLabel: string;\n  billingId?: string;\n  namesClassName?: string;\n}) {\n  const n = plans.length;\n  const railRef = React.useRef<HTMLDivElement>(null);\n  const thumbRef = React.useRef<HTMLDivElement>(null);\n  const drag = React.useRef<{\n    id: number;\n    touch: boolean;\n    moved: boolean;\n    /** px between the pointer and the ball's center when the ball was taken hold of. */\n    offset: number;\n    x: number;\n    y: number;\n    /** A finger only drags once it's clearly moving sideways; until then it may be a scroll. */\n    engaged: boolean;\n  } | null>(null);\n  const [dragging, setDragging] = React.useState(false);\n  // The focus halo is for keyboard use only. A press focuses the thumb from code (so arrows work after a click),\n  // which browsers can count as focus-visible, so the halo is tracked here instead.\n  const [ring, setRing] = React.useState(false);\n  const pressedAt = React.useRef(-Infinity);\n\n  const fill = useTransform(pos, (p) => `scaleX(${p})`);\n  // In container units of the rail, so the small thumb moves and no full-width layer is pushed past the edge.\n  const thumbX = useTransform(pos, (p) => `translateX(calc(${p} * 100cqw))`);\n\n  const full = format(locale, {});\n  const compact = format(locale, {\n    notation: \"compact\",\n    maximumFractionDigits: 1,\n  });\n  const valueText = (u: number) => `${full.format(u)} ${unit}, ${fitLabel}: ${plans[fitFor(plans, u, billingId)].name}`;\n\n  // Kept out of React: the slider's numbers change every frame of a drag.\n  useMotionValueEvent(usage, \"change\", (u) => {\n    thumbRef.current?.setAttribute(\"aria-valuenow\", String(u));\n    thumbRef.current?.setAttribute(\"aria-valuetext\", valueText(u));\n  });\n\n  // Where a glide in flight is headed, so a second PageUp goes on from there.\n  const heading = React.useRef<number | null>(null);\n  const setTo = (p: number) => {\n    heading.current = null;\n    pos.stop();\n    pos.set(clamp(p, 0, 1));\n  };\n  const glideTo = (p: number, transition: typeof TIMING.jump | typeof TIMING.press) => {\n    if (reduced) return setTo(p);\n    const target = clamp(p, 0, 1);\n    heading.current = target;\n    animate(pos, target, transition).then(() => {\n      if (heading.current === target) heading.current = null;\n    });\n  };\n\n  const onBallAt = (clientX: number) => {\n    const rect = railRef.current?.getBoundingClientRect();\n    return !!rect && Math.abs(clientX - (rect.left + pos.get() * rect.width)) <= FIT.grab;\n  };\n  const at = (clientX: number) => {\n    const rect = railRef.current?.getBoundingClientRect();\n    return rect ? clamp((clientX - rect.left) / rect.width, 0, 1) : pos.get();\n  };\n\n  const onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n    if (event.button !== 0) return;\n    const touch = event.pointerType === \"touch\";\n    const rect = railRef.current?.getBoundingClientRect();\n    const center = rect ? rect.left + pos.get() * rect.width : event.clientX;\n    const onBall = Math.abs(event.clientX - center) <= FIT.grab;\n    drag.current = {\n      id: event.pointerId,\n      touch,\n      moved: false,\n      offset: onBall ? event.clientX - center : 0,\n      x: event.clientX,\n      y: event.clientY,\n      engaged: !touch,\n    };\n    event.currentTarget.setPointerCapture(event.pointerId);\n    pressedAt.current = event.timeStamp;\n    setRing(false);\n    thumbRef.current?.focus({ preventScroll: true });\n    setDragging(true);\n    if (onBall) {\n      // Taking hold of the ball: it stays where it is until the pointer moves.\n      heading.current = null;\n      pos.stop();\n    } else if (!touch) {\n      // A finger might be starting a scroll, so touch waits to see which way it goes.\n      glideTo(at(event.clientX), TIMING.press);\n    }\n  };\n  const onPointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n    const current = drag.current;\n    if (!current || current.id !== event.pointerId) return;\n    if (!current.engaged) {\n      const dx = Math.abs(event.clientX - current.x);\n      const dy = Math.abs(event.clientY - current.y);\n      if (dx < FIT.slop && dy < FIT.slop) return;\n      if (dy >= dx) {\n        // A scroll: let the page have it.\n        drag.current = null;\n        setDragging(false);\n        return;\n      }\n      current.engaged = true;\n    }\n    current.moved = true;\n    setTo(at(event.clientX - current.offset));\n  };\n  const onPointerEnd = (event: React.PointerEvent<HTMLDivElement>) => {\n    const current = drag.current;\n    if (!current || current.id !== event.pointerId) return;\n    if (\n      event.type === \"pointerup\" &&\n      current.touch &&\n      !current.moved &&\n      current.offset === 0 &&\n      !onBallAt(event.clientX)\n    )\n      glideTo(at(event.clientX), TIMING.press);\n    drag.current = null;\n    setDragging(false);\n  };\n\n  // The allowance ticks, as rail positions.\n  const edges = scale.bounds.slice(1, n).map((_, k) => (k + 1) / n);\n  const nextEdge = (p: number, dir: 1 | -1) => {\n    const ends = [0, ...edges, 1];\n    return dir > 0 ? (ends.find((e) => e > p + 1e-4) ?? 1) : ([...ends].reverse().find((e) => e < p - 1e-4) ?? 0);\n  };\n\n  const onKeyDown = (event: React.KeyboardEvent) => {\n    setRing(true);\n    const p = heading.current ?? pos.get();\n    const up = event.key === \"ArrowRight\" || event.key === \"ArrowUp\";\n    const down = event.key === \"ArrowLeft\" || event.key === \"ArrowDown\";\n    if ((up || down) && !event.shiftKey) {\n      event.preventDefault();\n      // Step until the rounded usage actually changes, so no key press is lost.\n      const from = scale.toUsage(p);\n      let q = p;\n      do q = clamp(q + (up ? FIT.step : -FIT.step), 0, 1);\n      while (scale.toUsage(q) === from && q > 0 && q < 1);\n      setTo(q);\n      return;\n    }\n    const target =\n      event.key === \"Home\"\n        ? 0\n        : event.key === \"End\"\n          ? 1\n          : event.key === \"PageUp\" || (up && event.shiftKey)\n            ? nextEdge(p, 1)\n            : event.key === \"PageDown\" || (down && event.shiftKey)\n              ? nextEdge(p, -1)\n              : null;\n    if (target === null) return;\n    event.preventDefault();\n    glideTo(target, TIMING.jump);\n  };\n\n  // A plan's name jumps to its allowance; a quoted plan's to the middle of its stretch.\n  const jumpTo = (i: number) => {\n    const included = plans[i].included;\n    const quoted = typeof priceFor(plans[i]) !== \"number\" || included === undefined;\n    glideTo(quoted ? (i + 0.5) / n : scale.toPos(included), TIMING.jump);\n  };\n\n  const now = usage.get();\n\n  return (\n    <div>\n      <div\n        ref={railRef}\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={onPointerEnd}\n        onPointerCancel={onPointerEnd}\n        className=\"@container relative h-11 cursor-pointer touch-pan-y select-none\"\n      >\n        <div className=\"absolute inset-x-0 top-1/2 h-1.5 -translate-y-1/2 overflow-hidden rounded-full bg-zinc-950/[0.08] dark:bg-white/10\">\n          <motion.div style={{ transform: fill }} className=\"absolute inset-0 origin-left bg-(--plan-fit-accent)\" />\n        </div>\n        {edges.map((edge) => (\n          <span\n            key={edge}\n            aria-hidden\n            style={{ left: `${edge * 100}%` }}\n            className=\"absolute top-1/2 h-3.5 w-px -translate-x-1/2 -translate-y-1/2 bg-zinc-950/25 dark:bg-white/25\"\n          />\n        ))}\n        <motion.div\n          ref={thumbRef}\n          role=\"slider\"\n          tabIndex={0}\n          aria-label={`${unit[0]?.toUpperCase() ?? \"\"}${unit.slice(1)} per month`}\n          aria-valuemin={scale.bounds[0]}\n          aria-valuemax={scale.bounds[n]}\n          aria-valuenow={now}\n          aria-valuetext={valueText(now)}\n          aria-orientation=\"horizontal\"\n          onKeyDown={onKeyDown}\n          onFocus={(event) => setRing(event.timeStamp - pressedAt.current > TIMING.pressFocus)}\n          onBlur={() => setRing(false)}\n          data-dragging={dragging || undefined}\n          data-ring={ring || undefined}\n          style={{ transform: thumbX }}\n          className=\"group/thumb absolute top-1/2 left-0 size-[22px] -translate-1/2 rounded-full outline-none\"\n        >\n          {/* The ball grows while held. It's a child so the growth can't scale the slide along the rail. */}\n          <span\n            aria-hidden\n            className={`block size-full rounded-full bg-(--plan-fit-accent) shadow-[0_0_0_3px_var(--plan-fit-surface),0_2px_6px_rgb(0_0_0/0.22)] transition-[scale,box-shadow] ${TIMING.ball} group-data-[ring]/thumb:shadow-[0_0_0_3px_var(--plan-fit-surface),0_0_0_7px_color-mix(in_oklab,var(--plan-fit-accent)_35%,transparent)] group-data-[dragging]/thumb:scale-[1.15] dark:shadow-[0_0_0_3px_var(--plan-fit-surface),0_2px_6px_rgb(0_0_0/0.5)] dark:group-data-[ring]/thumb:shadow-[0_0_0_3px_var(--plan-fit-surface),0_0_0_7px_color-mix(in_oklab,var(--plan-fit-accent)_45%,transparent)]`}\n          />\n        </motion.div>\n      </div>\n\n      {/* Allowances on their ticks, plan names under their stretches. */}\n      <div aria-hidden className=\"relative h-4 text-[11px] font-medium text-zinc-400 tabular-nums dark:text-zinc-500\">\n        {edges.map((edge, k) => (\n          <span key={edge} style={{ left: `${edge * 100}%` }} className=\"absolute top-0 -translate-x-1/2\">\n            {compact.format(scale.bounds[k + 1])}\n          </span>\n        ))}\n      </div>\n      <div className={cn(\"mt-1 grid\", namesClassName)} style={{ gridTemplateColumns: `repeat(${n}, minmax(0, 1fr))` }}>\n        {plans.map((plan, i) => (\n          <button\n            key={plan.id}\n            type=\"button\"\n            tabIndex={-1}\n            onClick={() => jumpTo(i)}\n            className={cn(\n              `mx-auto max-w-full truncate rounded-md px-1.5 py-0.5 text-[13px] transition-colors ${TIMING.accentEase}`,\n              i === fit\n                ? `font-semibold text-(--plan-fit-accent-text) ${TIMING.accentIn}`\n                : `text-zinc-500 ${TIMING.accentOut} hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white`,\n            )}\n          >\n            {plan.name}\n          </button>\n        ))}\n      </div>\n    </div>\n  );\n}\n\n/* ── PlanCard ───────────────────────────────────────────── */\n\n// One plan. Its price, caption and meter are motion values computed from the\n// usage and the billing switch, so dragging never re-renders it.\nconst PlanCard = React.memo(function PlanCard({\n  plan,\n  fit,\n  usage,\n  billingAt,\n  billing,\n  billingId,\n  unit,\n  currency,\n  locale,\n  period,\n  fitLabel,\n  limit,\n}: {\n  plan: PlanFitPlan;\n  fit: boolean;\n  usage: MotionValue<number>;\n  billingAt: MotionValue<number>;\n  billing?: PlanFitBilling[];\n  billingId?: string;\n  unit: string;\n  currency: string;\n  locale: string;\n  period: string;\n  fitLabel: string;\n  limit: number;\n}) {\n  const cents = hasCents(plan);\n  const money = format(locale, {\n    style: \"currency\",\n    currency,\n    minimumFractionDigits: cents ? 2 : 0,\n    maximumFractionDigits: cents ? 2 : 0,\n  });\n  const full = format(locale, {});\n  const compact = format(locale, {\n    notation: \"compact\",\n    maximumFractionDigits: 1,\n  });\n\n  const reading = (u: number, b: number) => read(plan, priceAt(plan, billing, b), u, limit);\n  const inputs = [usage, billingAt];\n\n  const price = useTransform(inputs, ([u, b]: number[]) => {\n    const base = priceAt(plan, billing, b);\n    if (typeof base !== \"number\") return base;\n    const r = read(plan, base, u, limit);\n    return money.format(r.kind === \"over\" ? r.total : base);\n  });\n  const overage = useTransform(inputs, ([u, b]: number[]) => {\n    const r = reading(u, b);\n    return r.kind === \"over\" ? `+${money.format(r.extra)} for ${compact.format(r.over)} more ${unit}` : \"\";\n  });\n  const showOverage = useTransform(overage, (text) => (text ? 1 : 0));\n  const showCaption = useTransform(overage, (text) => (text ? 0 : 1));\n  const dim = useTransform(inputs, ([u, b]: number[]) => (reading(u, b).kind === \"outgrown\" ? 0.45 : 1));\n\n  // The meter's full width is the larger of usage and allowance.\n  const meter = useTransform(inputs, ([u, b]: number[]) => {\n    const r = reading(u, b);\n    if (r.kind === \"within\") return `${compact.format(r.usage)} of ${compact.format(r.included)} ${unit}`;\n    if (r.kind === \"over\") return `${compact.format(r.usage - r.included)} ${unit} over`;\n    if (r.kind === \"outgrown\") return `Tops out at ${compact.format(r.limit)}`;\n    return \"Any volume\";\n  });\n  const fill = useTransform(inputs, ([u, b]: number[]) => {\n    const r = reading(u, b);\n    const share = r.kind === \"within\" ? r.usage / r.included : r.kind === \"over\" ? r.included / r.usage : 1;\n    return `scaleX(${clamp(share, 0, 1)})`;\n  });\n  const tick = useTransform(inputs, ([u, b]: number[]) => {\n    const r = reading(u, b);\n    return `translateX(calc(${r.kind === \"over\" ? r.included / r.usage : 1} * 100cqw))`;\n  });\n  const hatch = useTransform(inputs, ([u, b]: number[]) => (reading(u, b).kind === \"over\" ? 1 : 0));\n\n  const quoted = typeof priceFor(plan, billingId) !== \"number\" || plan.included === undefined;\n  const caption = isPerOption<React.ReactNode>(plan.caption)\n    ? billingId !== undefined\n      ? plan.caption[billingId]\n      : Object.values(plan.caption)[0]\n    : plan.caption;\n\n  const Cta = plan.cta?.href ? \"a\" : \"button\";\n\n  return (\n    <motion.div\n      data-plan-fit-card=\"\"\n      data-fit={fit}\n      style={{ opacity: dim }}\n      className={`group/card relative flex w-(--plan-fit-card) shrink-0 snap-center flex-col p-5 transition-opacity ${TIMING.outgrow} @[64rem]/plan-fit:p-6`}\n    >\n      <h3 className=\"truncate pr-16 text-[15px] font-semibold tracking-[-0.01em]\">\n        {plan.name}\n        {fit && <span className=\"sr-only\"> ({fitLabel})</span>}\n      </h3>\n      <span\n        aria-hidden\n        className={cn(\n          `pointer-events-none absolute top-5 right-5 rounded-full bg-(--plan-fit-accent) px-2 py-0.5 text-[11px] leading-4 font-semibold text-(--plan-fit-accent-ink) transition-[opacity,scale] ${TIMING.accentEase} motion-reduce:scale-100 @[64rem]/plan-fit:top-6 @[64rem]/plan-fit:right-6`,\n          fit ? `scale-100 opacity-100 ${TIMING.accentIn}` : `scale-90 opacity-0 ${TIMING.accentOut}`,\n        )}\n      >\n        {fitLabel}\n      </span>\n      {plan.pitch && (\n        <p className=\"mt-1.5 line-clamp-2 min-h-10 text-[13px] leading-5 text-pretty text-zinc-500 dark:text-zinc-400\">\n          {plan.pitch}\n        </p>\n      )}\n\n      <p className=\"mt-5 flex items-baseline gap-1 whitespace-nowrap\">\n        <motion.span className=\"text-[1.875rem] leading-none font-semibold tracking-[-0.035em] tabular-nums @[64rem]/plan-fit:text-[2.125rem]\">\n          {price}\n        </motion.span>\n        {!quoted && <span className=\"text-[13px] text-zinc-500 dark:text-zinc-400\">{period}</span>}\n      </p>\n      <p className=\"relative mt-2 grid h-4 text-xs leading-4\">\n        <motion.span\n          style={{ opacity: showCaption }}\n          className=\"col-start-1 row-start-1 truncate text-zinc-500 dark:text-zinc-400\"\n        >\n          {caption}\n        </motion.span>\n        <motion.span\n          style={{ opacity: showOverage }}\n          className={`col-start-1 row-start-1 truncate font-medium text-zinc-700 transition-colors group-data-[fit=true]/card:text-(--plan-fit-accent-text) dark:text-zinc-300 ${TIMING.accent}`}\n        >\n          {overage}\n        </motion.span>\n      </p>\n\n      {/* The meter: usage against this plan's allowance. */}\n      <div className=\"mt-5\">\n        <motion.p className=\"text-xs font-medium text-zinc-600 tabular-nums dark:text-zinc-300\">{meter}</motion.p>\n        <div aria-hidden className={cn(\"@container relative mt-2 h-1.5\", quoted && \"invisible\")}>\n          <div className=\"absolute inset-0 overflow-hidden rounded-full bg-zinc-950/[0.07] dark:bg-white/[0.08]\">\n            <motion.div\n              style={{ opacity: hatch }}\n              className={cn(\n                \"absolute inset-0 bg-[repeating-linear-gradient(-45deg,currentColor_0_2px,transparent_2px_5px)] text-zinc-400 transition-colors group-data-[fit=true]/card:text-(--plan-fit-accent) dark:text-zinc-500\",\n                TIMING.accent,\n              )}\n            />\n            <motion.div\n              style={{ transform: fill }}\n              className={cn(\n                \"absolute inset-0 origin-left bg-zinc-800 transition-colors group-data-[fit=true]/card:bg-(--plan-fit-accent) dark:bg-zinc-300\",\n                TIMING.accent,\n                quoted && \"opacity-0\",\n              )}\n            />\n          </div>\n          {!quoted && (\n            <motion.span\n              style={{ transform: tick }}\n              className=\"absolute -inset-y-[3px] left-0 w-0.5 -translate-x-1/2 rounded-full bg-zinc-950/75 dark:bg-white/80\"\n            />\n          )}\n        </div>\n      </div>\n\n      {plan.cta && (\n        <Cta\n          {...(plan.cta.href ? { href: plan.cta.href } : { type: \"button\" as const })}\n          onClick={plan.cta.onClick}\n          className={cn(\n            \"mt-6 flex h-9 items-center justify-center rounded-full px-4 text-[13px] font-semibold outline-none focus-visible:ring-2 focus-visible:ring-(--plan-fit-accent) focus-visible:ring-offset-2 focus-visible:ring-offset-(--plan-fit-surface) active:scale-[0.97]\",\n            // The fill follows the accent timing; hover and press answer at once.\n            fit\n              ? `bg-(--plan-fit-accent) text-(--plan-fit-accent-ink) ${TIMING.ctaIn} hover:brightness-[0.94]`\n              : `bg-zinc-950/[0.05] text-zinc-900 ${TIMING.ctaOut} hover:bg-zinc-950/[0.09] dark:bg-white/[0.07] dark:text-white dark:hover:bg-white/[0.12]`,\n          )}\n        >\n          {plan.cta.label}\n        </Cta>\n      )}\n\n      <div className=\"mt-6 border-t border-zinc-950/[0.07] pt-5 dark:border-white/[0.08]\">\n        <p className=\"text-[13px] font-medium\">\n          {plan.included !== undefined ? `${full.format(plan.included)} ${unit}` : `Any number of ${unit}`}\n        </p>\n        {plan.features && plan.features.length > 0 && (\n          <ul className=\"mt-3 space-y-2 text-[13px] leading-5 text-zinc-600 dark:text-zinc-400\">\n            {plan.features.map((feature, i) => (\n              <li key={i} className=\"flex gap-2\">\n                <svg\n                  viewBox=\"0 0 16 16\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth={1.8}\n                  aria-hidden\n                  className={cn(\n                    \"mt-0.5 size-4 shrink-0 text-zinc-400 transition-colors group-data-[fit=true]/card:text-(--plan-fit-accent) dark:text-zinc-500\",\n                    TIMING.accent,\n                  )}\n                >\n                  <path d=\"m3.5 8.5 3 3 6-7\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n                </svg>\n                <span className=\"min-w-0\">{feature}</span>\n              </li>\n            ))}\n          </ul>\n        )}\n      </div>\n    </motion.div>\n  );\n});\n\n/* ── PlanFit ────────────────────────────────────────────── */\n\nexport function PlanFit({\n  plans,\n  unit = \"credits\",\n  readoutLabel,\n  defaultValue,\n  min = 0,\n  max,\n  billing,\n  defaultBilling,\n  currency = \"USD\",\n  locale = \"en-US\",\n  period = \"/mo\",\n  fitLabel = \"Best fit\",\n  onValueChange,\n  onFitChange,\n  glideDuration = TIMING.glide.visualDuration,\n  glideBounce = TIMING.glide.bounce,\n  countDuration = TIMING.count.duration,\n  className,\n  ...props\n}: PlanFitProps) {\n  const n = plans.length;\n  const largest = Math.max(0, ...plans.map((plan) => plan.included ?? 0));\n  const top = max ?? plans[n - 1]?.included ?? Math.max(1, largest * 4);\n  const scale = React.useMemo(() => createScale(plans, min, top), [plans, min, top]);\n  const reduced = useReduced();\n\n  const pos = useMotionValue(\n    defaultValue !== undefined ? scale.toPos(defaultValue) : (Math.min(1, n - 1) + FIT.start) / Math.max(1, n),\n  );\n  const usage = useMotionValue(scale.toUsage(pos.get()));\n\n  const [billingIndex, setBillingIndex] = React.useState(() =>\n    Math.max(0, billing?.findIndex((option) => option.id === defaultBilling) ?? 0),\n  );\n  const billingId = billing?.[billingIndex]?.id;\n  const billingAt = useMotionValue(billingIndex);\n\n  const [fit, setFit] = React.useState(() => fitFor(plans, usage.get(), billingId));\n  const [moved, setMoved] = React.useState(false);\n  const fitRef = React.useRef(fit);\n\n  const settle = (next: number) => {\n    if (next === fitRef.current) return;\n    fitRef.current = next;\n    setFit(next);\n    setMoved(true);\n    onFitChange?.(plans[next].id);\n  };\n\n  useMotionValueEvent(pos, \"change\", (p) => usage.set(scale.toUsage(p)));\n  useMotionValueEvent(usage, \"change\", (u) => {\n    onValueChange?.(u);\n    settle(fitFor(plans, u, billingId));\n  });\n\n  const selectBilling = (i: number) => {\n    if (!billing || i === billingIndex) return;\n    setBillingIndex(i);\n    if (reduced) {\n      billingAt.stop();\n      billingAt.set(i);\n    } else {\n      animate(billingAt, i, { ...TIMING.count, duration: Math.max(0, countDuration) });\n    }\n    settle(fitFor(plans, usage.get(), billing[i].id));\n  };\n\n  // Tell the parent which plan fits at first, so a button beside it can name it.\n  const reportFit = React.useRef(onFitChange);\n  React.useEffect(() => {\n    reportFit.current = onFitChange;\n  });\n  const firstFit = React.useRef(plans[fit]?.id);\n  React.useEffect(() => {\n    if (firstFit.current) reportFit.current?.(firstFit.current);\n  }, []);\n\n  // In the swipe row, bring each new fit to the middle. The first time, without a scroll.\n  const scrollerRef = React.useRef<HTMLDivElement>(null);\n  const scrolled = React.useRef(false);\n  React.useEffect(() => {\n    const scroller = scrollerRef.current;\n    if (!scroller) return;\n    const instant = !scrolled.current || reduced;\n    scrolled.current = true;\n    if (scroller.scrollWidth <= scroller.clientWidth + 1) return;\n    const card = scroller.querySelectorAll<HTMLElement>(\"[data-plan-fit-card]\")[fit];\n    if (!card) return;\n    scroller.scrollTo({\n      left: card.offsetLeft - (scroller.clientWidth - card.offsetWidth) / 2,\n      behavior: instant ? \"auto\" : \"smooth\",\n    });\n  }, [fit, reduced]);\n\n  const row = ROW[n] ?? { root: \"\", track: \"\", names: \"\" };\n  const readout = useTransform(usage, (u) => format(locale, {}).format(u));\n\n  if (n === 0) return null;\n\n  return (\n    <div\n      className={cn(\n        \"@container/plan-fit relative w-full text-zinc-950 [--plan-fit-accent-ink:#0a0a0a] [--plan-fit-accent-text:color-mix(in_oklab,var(--plan-fit-accent)_82%,#000)] [--plan-fit-accent:#ff4f12] [--plan-fit-card:min(17rem,80cqi)] [--plan-fit-surface:#ffffff] dark:text-white dark:[--plan-fit-accent-text:var(--plan-fit-accent)] dark:[--plan-fit-surface:#09090b]\",\n        className,\n      )}\n      {...props}\n    >\n      {/* A container query can't style its own container, so the row width is set one level down. */}\n      <div className={row.root}>\n        <div className=\"flex flex-wrap items-end justify-between gap-x-6 gap-y-4\">\n          <div className=\"min-w-0\">\n            <motion.p className=\"text-[2.75rem] leading-none font-semibold tracking-[-0.045em] tabular-nums @[40rem]/plan-fit:text-[3.5rem]\">\n              {readout}\n            </motion.p>\n            <p className=\"mt-2.5 text-sm text-zinc-500 dark:text-zinc-400\">{readoutLabel ?? `${unit} per month`}</p>\n          </div>\n          {billing && billing.length > 1 && (\n            <BillingSwitch options={billing} index={billingIndex} reduced={reduced} onSelect={selectBilling} />\n          )}\n        </div>\n\n        <div className=\"mt-6\">\n          <Rail\n            plans={plans}\n            scale={scale}\n            pos={pos}\n            usage={usage}\n            fit={fit}\n            unit={unit}\n            locale={locale}\n            reduced={reduced}\n            fitLabel={fitLabel}\n            billingId={billingId}\n            namesClassName={row.names}\n          />\n        </div>\n\n        <div\n          ref={scrollerRef}\n          className=\"mt-8 snap-x snap-mandatory overflow-x-auto overscroll-x-contain rounded-2xl bg-(--plan-fit-surface) shadow-[0_0_0_1px_rgb(9_9_11/0.09),0_1px_2px_rgb(9_9_11/0.04)] [scrollbar-width:none] dark:shadow-[0_0_0_1px_rgb(255_255_255/0.1)] [&::-webkit-scrollbar]:hidden\"\n        >\n          <div className={cn(\"relative flex w-max\", row.track)}>\n            {/* Dividers under the highlight, so its ring is never crossed by one. */}\n            <div aria-hidden className=\"pointer-events-none absolute inset-0 flex\">\n              {plans.map((plan, i) => (\n                <div\n                  key={plan.id}\n                  className={cn(\n                    \"w-(--plan-fit-card) shrink-0\",\n                    i > 0 && \"border-l border-zinc-950/[0.08] dark:border-white/[0.09]\",\n                  )}\n                />\n              ))}\n            </div>\n\n            <motion.div\n              key={reduced ? `fit-${fit}` : \"fit\"}\n              aria-hidden\n              initial={reduced && moved ? { opacity: 0, transform: `translateX(${fit * 100}%)` } : false}\n              animate={{ opacity: 1, transform: `translateX(${fit * 100}%)` }}\n              transition={\n                reduced\n                  ? TIMING.fade\n                  : { ...TIMING.glide, visualDuration: Math.max(0.01, glideDuration), bounce: clamp(glideBounce, 0, 1) }\n              }\n              className=\"pointer-events-none absolute inset-y-0 left-0 w-(--plan-fit-card) rounded-2xl bg-[linear-gradient(to_bottom,color-mix(in_oklab,var(--plan-fit-accent)_9%,var(--plan-fit-surface)),color-mix(in_oklab,var(--plan-fit-accent)_3%,var(--plan-fit-surface))_70%)] shadow-[inset_0_0_0_1.5px_var(--plan-fit-accent),0_18px_40px_-24px_color-mix(in_oklab,var(--plan-fit-accent)_70%,transparent)] will-change-transform dark:bg-[linear-gradient(to_bottom,color-mix(in_oklab,var(--plan-fit-accent)_15%,var(--plan-fit-surface)),color-mix(in_oklab,var(--plan-fit-accent)_5%,var(--plan-fit-surface))_70%)]\"\n            ></motion.div>\n\n            {plans.map((plan, i) => (\n              <PlanCard\n                key={plan.id}\n                plan={plan}\n                fit={i === fit}\n                usage={usage}\n                billingAt={billingAt}\n                billing={billing}\n                billingId={billingId}\n                unit={unit}\n                currency={currency}\n                locale={locale}\n                period={period}\n                fitLabel={fitLabel}\n                limit={limitOf(plans, i)}\n              />\n            ))}\n          </div>\n        </div>\n      </div>\n\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {fitLabel}: {plans[fit]?.name}\n      </p>\n    </div>\n  );\n}\n"
    }
  ]
}