KQ-1771 · CHANGE/PREVIEW

Change Preview

A plan-change preview with the plans as a radio group and the billing period drawn as a timeline beneath: the days used, and from the change date the credit for the unused plan and the charge for the new one. Choosing a plan slides the marker to the change date on springs.snap, then the credit and charge segments grow from it toward the period's end on springs.glide, the credit a beat ahead, while the amount due today rolls to its new figure; a cheaper plan that waits for the period's end gets a dashed marker there. Every position is a CSS variable written by a motion value, so the same values drive the horizontal timeline and the vertical one at phone widths; the radios take the arrow keys, Home and End.

KQ-1771 · CHANGE/PREVIEW

Change plan

Fernworks · on Team · billed monthly

Billing period · Oct 1 – Oct 31Changes todaySwitch to Business today: $23.71 credit for unused Team, $47.90 for 15 days of Business, $1.94 tax. $26.13 due today, then $99.00 a month from Nov 1.
CreditUnused Team, 15 days
−$23.71
ChargeBusiness, 15 days
+$47.90
Tax8% of the difference
+$1.94

Due today

$26.13

Then $99.00 a month from Nov 1

Business · on team · due today $26.13

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/change-preview

Props

currentPlan
string
default "team"
The plan the account is on now.
plans
ChangePlan[]
default defaultChangePlans
The plans on offer, cheapest first: { id, name, price, blurb? }.
period
ChangePeriod
default Oct 1 – Nov 1 2026
The current billing period: { start, end }.
now
number
default Oct 17 2026, 14:00 UTC
The moment the preview is made, in ms.
value / defaultValue
string
default — / "business"
The chosen plan id, controlled or initial.
onPreview
(plan) => void | Promise
default —
Fires from the press or key that chose a plan; a returned promise shows Calculating…, its figures replace the computed ones and a rejection is stated.
onConfirm
(plan, quote) => void | Promise
default —
Confirm was pressed with the quote; a returned promise holds the button.
onCancel
() => void
default —
Keep was pressed: the choice goes back to the current plan.
errors
{ credit?, charge?, tax? }
default —
Parts the host could not calculate, each with its reason; Confirm waits until they are known.
taxRate
number
default 0
Tax on what is due today, as a fraction.
downgradeAt
"period-end" | "now"
default "period-end"
When a cheaper plan starts.
account
string
default "Fernworks"
The account changing plans.
status
"ready" | "loading" | "error"
default "ready"
Whether the plans have arrived.
onRetry
() => void
default —
Try again was pressed after the plans failed to load.
timelineStyle
"track" | "rail" | "days"
default "track"
How the billing period is drawn: thick lanes, hairline rails, or one cell per day.
density
"compact" | "cozy" | "roomy"
default "cozy"
Padding, gaps and the plan cards' height.
accent
"cobalt" | "plum" | "mint" | "ink" | string
default "cobalt"
Selection, charge and Confirm: a named pigment or any CSS colour.
formatMoney
(amount) => string
default Intl USD
How money is written.
disabled
boolean
default false
Plans can be compared but not changed.
className
string
default —
Classes for the panel; it caps at 36rem tall and scrolls inside past that.