# Meterline, Usage billing

> A usage-based account page: the balance in a wallet, spend running on a meter, caps on breakers, the ledger, and API keys that expire on a library slip. A whole screen for a Next.js app, at /billing-desk.

A shadcn/ui block from the [@bevel](https://bevel-ui.vercel.app/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/bevel/billing-desk).

- Page, with a live preview: https://shadcn.exchange/r/bevel/billing-desk
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/bevel/billing-desk-light-6e1d0436613a961b.png
- Registry item: https://bevel-ui.vercel.app/r/billing-desk.json
- Categories: blocks
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/bevel/billing-desk.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `https://bevel-ui.vercel.app/r/bevel.json`, `https://bevel-ui.vercel.app/r/amount.json`, `https://bevel-ui.vercel.app/r/breaker.json`, `https://bevel-ui.vercel.app/r/due-date-slip.json`, `https://bevel-ui.vercel.app/r/ledger.json`, `https://bevel-ui.vercel.app/r/meter.json`, `https://bevel-ui.vercel.app/r/payment-card.json`, `https://bevel-ui.vercel.app/r/receipt.json`, `https://bevel-ui.vercel.app/r/stamp.json`, `https://bevel-ui.vercel.app/r/ticket-sheet.json`, `https://bevel-ui.vercel.app/r/wallet.json`

## Files

### `registry/bevel/blocks/billing-desk/meterline/header.tsx`

```tsx
"use client"

import { Pause, Play } from "lucide-react"
import { PanelKey } from "../shared/environment"

/** The product's own line, printed along the top edge of the desk: name, account, the clock and the traffic key. */
export function MeterlineHeader({
  clock,
  running,
  canStart,
  onTraffic,
}: {
  clock: string
  running: boolean
  canStart: boolean
  onTraffic: () => void
}) {
  return (
    <header className="mx-auto font-[family-name:var(--font-ledger)] flex max-w-[1440px] flex-wrap items-center gap-x-6 gap-y-3 px-4 pt-6 pb-5 sm:px-8">
      <div className="flex min-w-0 flex-wrap items-baseline gap-x-4 gap-y-1">
        <p className="text-[26px] leading-none font-bold text-[#EADFC8] [text-shadow:0_1px_0_rgb(0_0_0/0.5)]">
          Meterline
        </p>
        <p className="text-[17px] text-[#B8AA8E]">Halvorsen Labs, pay as you go</p>
      </div>
      <div className="ml-auto flex items-center gap-4">
        <p className="text-[17px] text-[#B8AA8E]">
          17 Oct, <span className="text-[#EADFC8] tabular-nums">{clock}</span>
        </p>
        <PanelKey
          tone={running ? "plain" : "go"}
          onClick={onTraffic}
          disabled={!running && !canStart}
          className="gap-2 [&_svg]:size-4"
        >
          {running ? <Pause aria-hidden /> : <Play aria-hidden />}
          {running ? "Pause traffic" : "Start traffic"}
        </PanelKey>
      </div>
    </header>
  )
}
```

### `registry/bevel/blocks/billing-desk/meterline/meterline.tsx`

```tsx
"use client"

import * as React from "react"
import { demoMilestone } from "../shared/events"
import { ArrowDown, CreditCard } from "lucide-react"
import { Amount } from "@/components/ui/amount"
import { Breaker, BreakerPanel, BreakerRow } from "@/components/ui/breaker"
import { DueDateSlip, DueDateSlipRenew, DueDateSlipStatus } from "@/components/ui/due-date-slip"
import {
  Ledger,
  LedgerAmount,
  LedgerBody,
  LedgerCell,
  LedgerHead,
  LedgerHeader,
  LedgerNumber,
  LedgerRow,
  LedgerTable,
  LedgerTitle,
} from "@/components/ui/ledger"
import { Meter, MeterButton, MeterFlag, MeterFooter, MeterHeader, MeterReading, MeterUnit } from "@/components/ui/meter"
import { PaymentCardBrand, PaymentCardNumber } from "@/components/ui/payment-card"
import {
  Receipt,
  ReceiptFooter,
  ReceiptHeader,
  ReceiptItem,
  ReceiptItems,
  ReceiptLabel,
  ReceiptMark,
  ReceiptMeta,
  ReceiptSeparator,
  ReceiptTitle,
  ReceiptTotal,
  ReceiptValue,
} from "@/components/ui/receipt"
import { Stamp, StampDetail } from "@/components/ui/stamp"
import { Ticket, TicketLabel, TicketNote, TicketSheet, TicketValue } from "@/components/ui/ticket-sheet"
import {
  Wallet,
  WalletAction,
  WalletAmount,
  WalletBalance,
  WalletCard,
  WalletCards,
  WalletDetail,
  WalletDetailLabel,
  WalletDetails,
  WalletDetailValue,
  WalletFooter,
  WalletHeader,
  WalletLabel,
  WalletPocket,
  WalletStatus,
} from "@/components/ui/wallet"
import { cn } from "@/lib/utils"
import { Legend, PanelKey, StatusStrip } from "../shared/environment"
import { ledgerFace } from "../shared/fonts"
import { MeterlineHeader } from "./header"
import {
  cardName,
  cards,
  clockText,
  flowing,
  formatUsd,
  hourlyRate,
  initialState,
  keyInfo,
  keyValid,
  LOW_BALANCE,
  MAX_STAMPS,
  NOW,
  packs,
  reducer,
  TICK_MS,
  totalSpent,
  trafficIds,
  type CircuitId,
  type State,
} from "./state"

const CENTS = { minimumFractionDigits: 2, maximumFractionDigits: 2 }
const WHOLE = { maximumFractionDigits: 0 }
const statusTone = { Settled: "black", Received: "green", Bonus: "blue" } as const

const circuitLabel: Record<CircuitId, string> = {
  main: "Account",
  production: "Production",
  staging: "Staging",
  batch: "Batch",
}

/** Each key slip lies at its own angle in the card file, as if dropped there. */
const slipTilt = ["-rotate-2", "rotate-1", "-rotate-1"]

/** The desk: walnut, lit from a lamp above, with the grain running across. */
const deskStyle: React.CSSProperties = {
  backgroundColor: "#4A3022",
  backgroundImage: [
    "radial-gradient(ellipse 75% 55% at 50% 0%, rgb(255 214 160 / 0.17), transparent 72%)",
    "radial-gradient(ellipse 120% 80% at 50% 120%, rgb(0 0 0 / 0.35), transparent 70%)",
    "repeating-linear-gradient(177deg, rgb(0 0 0 / 0.07) 0 1px, transparent 1px 6px)",
    "repeating-linear-gradient(182deg, rgb(255 230 200 / 0.035) 0 2px, transparent 2px 29px)",
  ].join(", "),
}

/** A legend tooled in gold leaf into the blotter's leather. */
function Tooled({ className, ...props }: React.ComponentProps<typeof Legend>) {
  return (
    <Legend
      className={cn(
        "font-[family-name:var(--font-ledger)] text-[19px] font-bold tracking-normal text-[#C9A44A] [text-shadow:0_1px_0_rgb(0_0_0/0.6),0_-1px_0_rgb(255_236_180/0.14)]",
        className
      )}
      {...props}
    />
  )
}

function runwayText(state: State) {
  if (state.balance <= 0) return "Out of credit"
  const rate = hourlyRate(state)
  if (!state.running || rate === 0) return "Not drawing"
  const minutes = Math.round((state.balance / rate) * 60)
  const h = Math.floor(minutes / 60)
  return h > 0 ? `About ${h} h ${minutes % 60} m` : `About ${minutes} m`
}

export function Meterline() {
  const [state, dispatch] = React.useReducer(reducer, initialState)
  const topUpRef = React.useRef<HTMLParagraphElement>(null)

  React.useEffect(() => {
    if (!state.running) return
    const timer = setInterval(() => {
      dispatch({
        type: "tick",
        draws: { production: Math.random(), staging: Math.random(), batch: Math.random() },
      })
    }, TICK_MS)
    return () => clearInterval(timer)
  }, [state.running])

  const spent = totalSpent(state.circuits)
  const live = trafficIds.filter((id) => flowing(state, id))
  const metering = state.running && live.length > 0
  const walletStatus = state.balance <= 0 ? "empty" : state.balance < LOW_BALANCE ? "low" : "default"
  const pack = packs.find((p) => p.id === state.pack)!
  const expiringSoon = trafficIds.filter((id) => {
    const last = new Date(state.keys[id].at(-1)!)
    const days = (last.getTime() - NOW.getTime()) / 86_400_000
    return days >= 0 && days <= 7
  }).length
  const expired = trafficIds.filter((id) => !keyValid(state.keys[id])).length
  const keysNote = [expired > 0 && `${expired} expired`, expiringSoon > 0 && `${expiringSoon} due this week`]
    .filter(Boolean)
    .join(", ")
  const printed = state.receipt.no !== initialState.receipt.no

  const goToTopUp = () => {
    document.getElementById("top-up")?.scrollIntoView({ behavior: "smooth", block: "center" })
    topUpRef.current?.focus({ preventScroll: true })
  }

  const breakerDescription = (id: CircuitId) => {
    const c = state.circuits[id]
    if (id !== "main" && !keyValid(state.keys[id])) return "Key expired"
    const used = id === "main" ? spent : c.spent
    return `${formatUsd(used)} of ${formatUsd(c.cap)}`
  }

  return (
    <div
      style={deskStyle}
      className={cn(ledgerFace.variable, "min-h-svh overflow-x-clip text-[#EADFC8]")}
    >
      <MeterlineHeader
        clock={clockText(state.clock)}
        running={state.running}
        canStart={state.balance > 0}
        onTraffic={() => dispatch({ type: state.running ? "pause" : "start" })}
      />

      <main className="mx-auto max-w-[1440px] px-2 pb-12 sm:px-8">
        {/* The blotter: green leather set into the walnut, stitched round its edge. */}
        <div
          className={cn(
            "bevel-leather relative rounded-[22px] px-4 py-7 sm:p-8 xl:p-12 [--bevel-material:#2E4A3B]",
            "shadow-[inset_0_2px_3px_rgb(0_0_0/0.45),inset_0_0_0_1px_rgb(0_0_0/0.35),0_1px_0_rgb(255_230_190/0.12),0_24px_48px_-24px_rgb(0_0_0/0.7)]"
          )}
        >
          <span
            aria-hidden
            className="bevel-stitch pointer-events-none absolute inset-2.5 rounded-[14px] [--bevel-stitch:rgb(201_164_74/0.55)]"
          />

          <div className="relative grid gap-x-12 gap-y-10 lg:grid-cols-[minmax(0,11fr)_minmax(0,13fr)] [&>*]:min-w-0">
            {/* The one line of news, on a small backlit display at the head of the desk. */}
            <div role="status" className="lg:col-start-2 lg:row-start-1">
              <StatusStrip className="font-[family-name:var(--font-ledger)] [&>p]:text-[16px] [&>p]:leading-snug [&>p]:tracking-normal sm:[&>p]:whitespace-normal">
                {state.event}
              </StatusStrip>
            </div>

            {/* The hero: the wallet, open, with the latest receipt tucked under it. */}
            <section aria-labelledby="ml-balance" className="flex flex-col gap-4 lg:col-start-1 lg:row-span-2 lg:row-start-1">
              <Tooled id="ml-balance">Balance</Tooled>
              <div className="flex flex-col items-center lg:items-start">
                <div
                  data-bevel-family="leather"
                  className="relative z-20 w-full max-w-sm -rotate-1 xl:[zoom:1.2] xl:min-[1400px]:[zoom:1.4]"
                >
                  <Wallet variant="noir" status={walletStatus} className="max-w-none">
                    <WalletCards value={state.card} onValueChange={(id) => dispatch({ type: "card", id })} aria-label="Card to charge">
                      {cards.map((card) => (
                        <WalletCard key={card.id} value={card.id} tone={card.tone}>
                          <PaymentCardBrand>{card.brand}</PaymentCardBrand>
                          <PaymentCardNumber last4={card.last4} />
                        </WalletCard>
                      ))}
                    </WalletCards>
                    <WalletPocket>
                      <WalletHeader>
                        <WalletLabel>Credit balance</WalletLabel>
                        <WalletStatus />
                      </WalletHeader>
                      <WalletBalance value={state.balance} currency="USD" />
                      <WalletDetails>
                        <WalletDetail>
                          <WalletDetailLabel>Spent today</WalletDetailLabel>
                          <WalletDetailValue>
                            <WalletAmount value={spent} currency="USD" />
                          </WalletDetailValue>
                        </WalletDetail>
                        <WalletDetail>
                          <WalletDetailLabel>Not yet settled</WalletDetailLabel>
                          <WalletDetailValue>
                            <WalletAmount value={state.pending.amount} currency="USD" />
                          </WalletDetailValue>
                        </WalletDetail>
                        <WalletDetail>
                          <WalletDetailLabel>Runway</WalletDetailLabel>
                          <WalletDetailValue>{runwayText(state)}</WalletDetailValue>
                        </WalletDetail>
                      </WalletDetails>
                      <WalletFooter>
                        <WalletAction onClick={goToTopUp}>
                          <ArrowDown /> Top up
                        </WalletAction>
                      </WalletFooter>
                    </WalletPocket>
                  </Wallet>
                </div>

                {/* Each top-up prints a fresh receipt that slides out from under the wallet. */}
                <div
                  key={state.receipt.no}
                  data-bevel-family="paper"
                  className={cn(
                    "relative z-10 -mt-5 w-full max-w-[18.5rem] rotate-2 self-end sm:mr-6 lg:mr-0 lg:self-center lg:translate-x-16",
                    printed &&
                      "transition-[translate,opacity] duration-(--bevel-duration-move) ease-(--bevel-ease-out) starting:-translate-y-16 starting:opacity-0 motion-reduce:transition-none"
                  )}
                >
                  <Receipt aria-label={`Receipt ${state.receipt.no}`}>
                    <ReceiptHeader>
                      <ReceiptTitle>Meterline credits</ReceiptTitle>
                      <ReceiptMeta>
                        {state.receipt.no}, {state.receipt.time}
                      </ReceiptMeta>
                    </ReceiptHeader>
                    <ReceiptSeparator />
                    <ReceiptItems>
                      {state.receipt.lines.map((line) => (
                        <ReceiptItem key={line.label}>
                          <ReceiptLabel>{line.label}</ReceiptLabel>
                          <ReceiptValue>{line.amount === 0 ? "Free" : <Amount value={line.amount} currency="USD" />}</ReceiptValue>
                        </ReceiptItem>
                      ))}
                      <ReceiptItem>
                        <ReceiptLabel>Card</ReceiptLabel>
                        <ReceiptValue>{state.receipt.card}</ReceiptValue>
                      </ReceiptItem>
                    </ReceiptItems>
                    <ReceiptSeparator />
                    <ReceiptTotal>
                      <ReceiptLabel>Charged</ReceiptLabel>
                      <ReceiptValue>
                        <Amount value={state.receipt.total} currency="USD" />
                      </ReceiptValue>
                    </ReceiptTotal>
                    <ReceiptMark>
                      <Stamp tone="green" animate={printed}>
                        Paid
                        <StampDetail>{state.receipt.stampDate}</StampDetail>
                      </Stamp>
                    </ReceiptMark>
                    <ReceiptFooter>billing@halvorsen.example</ReceiptFooter>
                  </Receipt>
                </div>
              </div>
            </section>

            {/* The instruments: the spend counter, and the breaker panel beside it. */}
            <div className="flex flex-col gap-10 lg:col-start-2 lg:row-start-2">
              <div className="grid gap-x-10 gap-y-10 xl:grid-cols-[minmax(0,22rem)_minmax(0,1fr)] [&>*]:min-w-0">
                <section aria-labelledby="ml-spend" className="flex flex-col gap-4">
                  <Tooled id="ml-spend">Spend today</Tooled>
                  <Meter
                    state={metering ? "running" : spent > 0 ? "stopped" : "idle"}
                    aria-labelledby="ml-spend"
                    className="mx-auto rotate-[0.6deg] sm:mx-0"
                  >
                    <MeterHeader className="justify-end">
                      <MeterFlag>{metering ? "Metering" : state.running ? "Blocked" : "Paused"}</MeterFlag>
                    </MeterHeader>
                    <div className="flex flex-col gap-1">
                      <MeterReading value={spent} currency="USD" format={CENTS} />
                      <MeterUnit>of {formatUsd(state.circuits.main.cap)} account cap</MeterUnit>
                    </div>
                    <div className="flex flex-col gap-0.5">
                      <MeterUnit>
                        <Amount value={state.requests} format={WHOLE} /> requests today
                      </MeterUnit>
                      <MeterUnit>
                        <Amount value={state.refused} format={WHOLE} /> refused by caps
                      </MeterUnit>
                    </div>
                    <MeterFooter>
                      <MeterButton disabled={state.pending.calls === 0} onClick={() => dispatch({ type: "settle" })}>
                        Settle now
                      </MeterButton>
                    </MeterFooter>
                  </Meter>
                </section>

                <section aria-labelledby="ml-caps" className="flex flex-col gap-4">
                  <Tooled id="ml-caps">Daily caps</Tooled>
                  <BreakerPanel aria-labelledby="ml-caps" className="w-fit max-w-full self-center sm:self-start">
                    <BreakerRow className="justify-center">
                      {(["main", ...trafficIds] as CircuitId[]).map((id) => {
                        const c = state.circuits[id]
                        const keyOk = id === "main" || keyValid(state.keys[id])
                        return (
                          <Breaker
                            key={id}
                            label={circuitLabel[id]}
                            description={breakerDescription(id)}
                            checked={c.on && keyOk}
                            tripped={c.tripped}
                            disabled={!keyOk}
                            onCheckedChange={(on) => dispatch({ type: "breaker", id, on })}
                          />
                        )
                      })}
                    </BreakerRow>
                  </BreakerPanel>
                </section>
              </div>

              {/* A sheet of credit tickets, and the key that pays for the one torn off. */}
              <section id="top-up" aria-labelledby="ml-top-up" className="flex scroll-mt-6 flex-col gap-4">
                <Tooled id="ml-top-up" ref={topUpRef} tabIndex={-1} className="rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-[#C9A44A]">
                  Top up
                </Tooled>
                <div className="flex flex-col items-center gap-6 sm:flex-row sm:gap-10 lg:flex-col lg:items-start xl:flex-row xl:items-center">
                  <div data-bevel-family="paper" className="w-full max-w-[21rem] -rotate-1 sm:w-[21rem] sm:shrink-0">
                    <TicketSheet aria-label="Credit pack" value={state.pack} onValueChange={(id) => dispatch({ type: "pack", id: id as string })}>
                      {packs.map((p) => (
                        <Ticket key={p.id} value={p.id} tone={p.tone}>
                          <TicketLabel>Credits</TicketLabel>
                          <TicketValue>${p.credits}</TicketValue>
                          <TicketNote>{p.note}</TicketNote>
                        </Ticket>
                      ))}
                    </TicketSheet>
                  </div>
                  <div className="flex w-full max-w-[21rem] flex-col gap-4 sm:w-auto">
                    <p className="font-[family-name:var(--font-ledger)] text-[17px] leading-relaxed text-[#B8AA8E]">
                      Adds{" "}
                      <span className="text-[#EADFC8]">
                        <Amount value={pack.credits + pack.bonus} currency="USD" />
                      </span>{" "}
                      to the balance, charged to <span className="text-[#EADFC8]">{cardName(state.card)}</span> from the wallet.
                    </p>
                    <PanelKey tone="go" onClick={() => {
                      dispatch({ type: "pay" })
                      demoMilestone("meterline", "top_up")
                    }} className="h-12 gap-2 self-start text-[16px] [&_svg]:size-4">
                      <CreditCard aria-hidden /> Pay {formatUsd(pack.credits)}
                    </PanelKey>
                  </div>
                </div>
              </section>
            </div>

            {/* The ledger book, open across the foot of the blotter, and the card file of key slips. */}
            <div className="grid gap-x-10 gap-y-10 lg:col-span-2 xl:grid-cols-[minmax(0,1fr)_auto] [&>*]:min-w-0">
              <section aria-labelledby="ml-ledger" className="flex flex-col gap-4">
                <Tooled id="ml-ledger">Ledger</Tooled>
                <div className="relative -rotate-[0.4deg]">
                  <div
                    data-bevel-family="paper"
                    className="bevel-leather rounded-[10px] p-2.5 shadow-[0_18px_30px_-14px_rgb(0_0_0/0.75)] [--bevel-material:#5B2A22]"
                  >
                    <Ledger>
                      <LedgerTitle>October 2026</LedgerTitle>
                      <LedgerTable>
                        <LedgerHeader>
                          <LedgerRow>
                            <LedgerHead>No.</LedgerHead>
                            <LedgerHead>Time</LedgerHead>
                            <LedgerHead>Entry</LedgerHead>
                            <LedgerHead className="text-right">Amount</LedgerHead>
                            <LedgerHead className="text-right">Balance</LedgerHead>
                            <LedgerHead>Status</LedgerHead>
                          </LedgerRow>
                        </LedgerHeader>
                        <LedgerBody>
                          {state.entries.map((entry) => (
                            <LedgerRow key={entry.no}>
                              <LedgerCell>
                                <LedgerNumber value={entry.no} />
                              </LedgerCell>
                              <LedgerCell className="whitespace-nowrap">{entry.time}</LedgerCell>
                              <LedgerCell className="whitespace-nowrap">{entry.what.replace(" · ", ", ")}</LedgerCell>
                              <LedgerCell className="text-right">
                                <LedgerAmount value={entry.amount} currency="USD" />
                              </LedgerCell>
                              <LedgerCell className="text-right">
                                <Amount value={entry.after} currency="USD" />
                              </LedgerCell>
                              <LedgerCell>
                                <Stamp tone={statusTone[entry.status]} className="px-1 py-0.5 text-(length:--bevel-fine)">
                                  {entry.status}
                                </Stamp>
                              </LedgerCell>
                            </LedgerRow>
                          ))}
                        </LedgerBody>
                      </LedgerTable>
                    </Ledger>
                  </div>
                  {/* A ribbon marker hanging from the book. */}
                  <span
                    aria-hidden
                    className="absolute right-14 -bottom-7 h-10 w-3 bg-[#9E2B25] shadow-[1px_2px_3px_rgb(0_0_0/0.4)] [clip-path:polygon(0_0,100%_0,100%_100%,50%_calc(100%-6px),0_100%)]"
                  />
                </div>
              </section>

              <section aria-labelledby="ml-keys" className="flex flex-col gap-4">
                <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
                  <Tooled id="ml-keys">API keys</Tooled>
                  {keysNote && <p className="font-[family-name:var(--font-ledger)] text-[17px] text-[#EADFC8]">{keysNote}</p>}
                </div>
                <div data-bevel-family="paper" className="flex flex-wrap justify-center gap-6 xl:flex-nowrap xl:justify-start xl:gap-0 xl:[&>*:not(:first-child)]:-ml-1">
                  {trafficIds.map((id, i) => {
                    const dates = state.keys[id]
                    const full = dates.length >= MAX_STAMPS
                    return (
                      <div
                        key={id}
                        className={cn(
                          "relative transition-[translate] duration-(--bevel-duration-small) ease-(--bevel-ease-out) hover:z-10 hover:-translate-y-1 focus-within:z-10 focus-within:-translate-y-1 motion-reduce:transition-none",
                          slipTilt[i % slipTilt.length]
                        )}
                      >
                        <DueDateSlip className="w-48" title={keyInfo[id].title.replace(" · ", ", ")} dates={dates} now={NOW}>
                          <DueDateSlipStatus />
                          <DueDateSlipRenew onClick={() => dispatch({ type: "renew", id })} disabled={full}>
                            {full ? "Slip full: rotate key" : "Renew 90 days"}
                          </DueDateSlipRenew>
                        </DueDateSlip>
                      </div>
                    )
                  })}
                </div>
              </section>
            </div>
          </div>
        </div>
      </main>
    </div>
  )
}
```

### `registry/bevel/blocks/billing-desk/meterline/state.tsx`

```tsx
/** Meterline's account state: one reducer so a tick of traffic moves the meter, wallet, caps and ledger together. */

export const NOW = new Date("2026-10-17T00:00:00Z")
const DAY = 86_400_000
/** Simulated seconds per tick. Traffic runs at 60x: one 600ms tick is 36 seconds of the day. */
export const TICK_MS = 600
export const SIM_SECONDS_PER_TICK = 36
const SETTLE_EVERY = 8
export const LOW_BALANCE = 20

export type TrafficId = "production" | "staging" | "batch"
export type CircuitId = "main" | TrafficId
export type KeyId = TrafficId

export const traffic: Record<TrafficId, { name: string; min: number; max: number; price: number }> = {
  production: { name: "Production", min: 180, max: 420, price: 0.0004 },
  staging: { name: "Staging", min: 20, max: 80, price: 0.0002 },
  batch: { name: "Batch", min: 60, max: 200, price: 0.0003 },
}
export const trafficIds = Object.keys(traffic) as TrafficId[]

/** How much a reset lifts a cap that's been hit, for the rest of the day. */
export const raiseBy: Record<CircuitId, number> = { main: 10, production: 5, staging: 2, batch: 2 }

export type Circuit = { on: boolean; tripped: boolean; cap: number; spent: number }

export type Entry = {
  no: number
  time: string
  what: string
  amount: number
  after: number
  status: "Settled" | "Received" | "Bonus"
}

export type ReceiptData = {
  no: string
  time: string
  card: string
  lines: { label: string; amount: number }[]
  total: number
  stampDate: string
}

export type Card = { id: string; brand: string; last4: string; tone: "sapphire" | "gold" | "graphite" }
export const cards: Card[] = [
  { id: "debit-8210", brand: "Debit", last4: "8210", tone: "gold" },
  { id: "visa-4417", brand: "Visa", last4: "4417", tone: "sapphire" },
]
export const cardName = (id: string) => {
  const card = cards.find((c) => c.id === id)!
  return `${card.brand} ··${card.last4}`
}

export const packs = [
  { id: "25", credits: 25, bonus: 0, tone: "vermilion", note: "Top-up" },
  { id: "50", credits: 50, bonus: 0, tone: "ultramarine", note: "Most bought" },
  { id: "100", credits: 100, bonus: 4, tone: "emerald", note: "+ $4 bonus" },
  { id: "250", credits: 250, bonus: 15, tone: "ochre", note: "+ $15 bonus" },
] as const

export const keyInfo: Record<KeyId, { title: string; uses: string }> = {
  production: { title: "Production · ml_live_…7c1e", uses: "Signs requests on the production circuit." },
  staging: { title: "Staging · ml_test_…09ab", uses: "Signs requests on the staging circuit." },
  batch: { title: "Batch worker · ml_live_…a3d0", uses: "Signs nightly and batch jobs." },
}
export const MAX_STAMPS = 6

export type State = {
  running: boolean
  clock: number
  ticks: number
  balance: number
  /** Balance as of the last ledger line, in exact cents. */
  ledgerBalance: number
  requests: number
  refused: number
  circuits: Record<CircuitId, Circuit>
  pending: { calls: number; amount: number }
  entries: Entry[]
  nextNo: number
  keys: Record<KeyId, string[]>
  card: string
  pack: string
  receipt: ReceiptData
  receiptNo: number
  event: string
}

const round2 = (n: number) => Math.round(n * 100) / 100
const usd = (n: number) =>
  n.toLocaleString("en-US", { style: "currency", currency: "USD" })
export const formatUsd = usd

const pad = (n: number) => String(n).padStart(2, "0")
export function clockText(seconds: number) {
  const s = Math.min(seconds, 23 * 3600 + 59 * 60)
  return `${pad(Math.floor(s / 3600))}:${pad(Math.floor((s % 3600) / 60))}`
}
const stampTime = (seconds: number) => `17 Oct ${clockText(seconds)}`

const iso = (d: Date) => d.toISOString().slice(0, 10)
const longDate = (d: Date) =>
  d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" })

export function keyValid(dates: string[]) {
  const last = dates.at(-1)
  return last ? new Date(last) >= NOW : false
}

export function totalSpent(circuits: Record<CircuitId, Circuit>) {
  return trafficIds.reduce((sum, id) => sum + circuits[id].spent, 0)
}

export function flowing(state: State, id: TrafficId) {
  const main = state.circuits.main
  const c = state.circuits[id]
  return main.on && !main.tripped && c.on && !c.tripped && keyValid(state.keys[id])
}

/** Expected spend per simulated hour from what's flowing right now. */
export function hourlyRate(state: State) {
  const perTick = trafficIds
    .filter((id) => flowing(state, id))
    .reduce((sum, id) => sum + ((traffic[id].min + traffic[id].max) / 2) * traffic[id].price, 0)
  return (perTick * 3600) / SIM_SECONDS_PER_TICK
}

export const initialState: State = {
  running: false,
  clock: 14 * 3600 + 2 * 60,
  ticks: 0,
  balance: 48.6,
  ledgerBalance: 48.6,
  requests: 36_212,
  refused: 0,
  circuits: {
    main: { on: true, tripped: false, cap: 25, spent: 0 },
    production: { on: true, tripped: false, cap: 15, spent: 10.8 },
    staging: { on: false, tripped: false, cap: 3, spent: 0 },
    batch: { on: true, tripped: false, cap: 6, spent: 3.69 },
  },
  pending: { calls: 0, amount: 0 },
  entries: [
    { no: 118, time: "17 Oct 13:58", what: "Usage · 4,812 calls", amount: -1.93, after: 48.6, status: "Settled" },
    { no: 117, time: "17 Oct 12:00", what: "Usage · 31,400 calls", amount: -12.56, after: 50.53, status: "Settled" },
    { no: 116, time: "16 Oct 23:59", what: "Usage · day close", amount: -54.91, after: 63.09, status: "Settled" },
    { no: 115, time: "12 Oct 10:41", what: "Credits · Visa ··4417", amount: 100, after: 118, status: "Received" },
  ],
  nextNo: 119,
  keys: {
    production: ["2026-04-23", "2026-07-22", "2026-10-21"],
    staging: ["2026-07-16", "2026-10-14"],
    batch: ["2026-11-07"],
  },
  card: "visa-4417",
  pack: "50",
  receipt: {
    no: "R-0412",
    time: "12 Oct 2026, 10:41",
    card: "Visa ··4417",
    lines: [{ label: "100 credits", amount: 100 }],
    total: 100,
    stampDate: "12 Oct",
  },
  receiptNo: 413,
  event: "Traffic paused. Start traffic to watch spend run.",
}

export type Action =
  | { type: "tick"; draws: Record<TrafficId, number> }
  | { type: "start" }
  | { type: "pause" }
  | { type: "settle" }
  | { type: "breaker"; id: CircuitId; on: boolean }
  | { type: "renew"; id: KeyId }
  | { type: "card"; id: string }
  | { type: "pack"; id: string }
  | { type: "pay" }

const MAX_ENTRIES = 12

/** Post unsettled usage to the ledger as one line. Keeps the ledger's arithmetic exact in cents. */
function settle(state: State): State {
  const posted = round2(state.pending.amount)
  if (state.pending.calls === 0 || posted < 0.01) return state
  const after = round2(state.ledgerBalance - posted)
  const entry: Entry = {
    no: state.nextNo,
    time: stampTime(state.clock),
    what: `Usage · ${state.pending.calls.toLocaleString("en-US")} calls`,
    amount: -posted,
    after,
    status: "Settled",
  }
  return {
    ...state,
    ledgerBalance: after,
    pending: { calls: 0, amount: state.pending.amount - posted },
    entries: [entry, ...state.entries].slice(0, MAX_ENTRIES),
    nextNo: state.nextNo + 1,
  }
}

function tick(state: State, draws: Record<TrafficId, number>): State {
  if (!state.running) return state
  const circuits = { ...state.circuits }
  let balance = state.balance
  let calls = 0
  let amount = 0
  let refused = state.refused
  let event: string | null = null
  let empty = false

  for (const id of trafficIds) {
    const c = circuits[id]
    const want = Math.round(traffic[id].min + draws[id] * (traffic[id].max - traffic[id].min))
    if (!c.on || !keyValid(state.keys[id])) continue
    if (c.tripped || circuits.main.tripped || !circuits.main.on) {
      refused += want
      continue
    }
    let cost = want * traffic[id].price
    let n = want
    const ownRoom = c.cap - c.spent
    const mainRoom = circuits.main.cap - totalSpent(circuits)
    const room = Math.min(ownRoom, mainRoom, balance)
    if (cost >= room) {
      n = Math.floor((want * Math.max(room, 0)) / cost)
      cost = Math.max(room, 0)
      refused += want - n
      if (room === balance) {
        empty = true
        event = "Out of credit. All traffic stopped. Top up to send requests again."
      } else if (room === ownRoom) {
        circuits[id] = { ...c, tripped: true }
        event = `${traffic[id].name} cap tripped at ${usd(c.cap)}. Requests on ${traffic[id].name.toLowerCase()} are refused until you reset it.`
      } else {
        circuits.main = { ...circuits.main, tripped: true }
        event = `Account cap tripped at ${usd(circuits.main.cap)}. Every request is refused until you reset the main breaker.`
      }
    }
    circuits[id] = { ...circuits[id], spent: circuits[id].spent + cost }
    balance = Math.max(0, balance - cost)
    calls += n
    amount += cost
    if (empty) break
  }

  const next: State = {
    ...state,
    circuits,
    balance: empty ? 0 : balance,
    running: !empty,
    clock: state.clock + SIM_SECONDS_PER_TICK,
    ticks: state.ticks + 1,
    requests: state.requests + calls,
    refused,
    pending: { calls: state.pending.calls + calls, amount: state.pending.amount + amount },
    event: event ?? state.event,
  }
  return event || next.ticks % SETTLE_EVERY === 0 ? settle(next) : next
}

export function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "tick":
      return tick(state, action.draws)
    case "start":
      if (state.balance <= 0) return state
      return { ...state, running: true, event: "Traffic running. Usage settles to the ledger every few minutes." }
    case "pause":
      return settle({ ...state, running: false, event: "Traffic paused." })
    case "settle":
      return settle(state)
    case "card":
      return { ...state, card: action.id }
    case "pack":
      return { ...state, pack: action.id }
    case "breaker": {
      const c = state.circuits[action.id]
      const name = action.id === "main" ? "Account" : traffic[action.id].name
      if (c.tripped && action.on) {
        const spent = action.id === "main" ? totalSpent(state.circuits) : c.spent
        const cap = spent >= c.cap - 0.005 ? c.cap + raiseBy[action.id] : c.cap
        return settle({
          ...state,
          circuits: { ...state.circuits, [action.id]: { ...c, tripped: false, on: true, cap } },
          event: `${name} cap reset${cap > c.cap ? ` and raised to ${usd(cap)} for today` : ""}.`,
        })
      }
      return settle({
        ...state,
        circuits: { ...state.circuits, [action.id]: { ...c, on: action.on, tripped: false } },
        event:
          action.id === "main"
            ? action.on
              ? "Main breaker on. Traffic can flow."
              : "Main breaker off. Every request is refused."
            : `${name} circuit switched ${action.on ? "on" : "off"}.`,
      })
    }
    case "renew": {
      const dates = state.keys[action.id]
      if (dates.length >= MAX_STAMPS) return state
      const last = new Date(dates.at(-1)!)
      const from = last < NOW ? NOW : last
      const due = new Date(from.getTime() + 90 * DAY)
      const wasExpired = !keyValid(dates)
      return {
        ...state,
        keys: { ...state.keys, [action.id]: [...dates, iso(due)] },
        event: `${traffic[action.id].name} key renewed to ${longDate(due)}.${
          wasExpired ? ` Switch the ${traffic[action.id].name.toLowerCase()} breaker on to send traffic.` : ""
        }`,
      }
    }
    case "pay": {
      const pack = packs.find((p) => p.id === state.pack)!
      const settled = settle(state)
      const time = stampTime(settled.clock)
      const card = cardName(settled.card)
      const afterCredits = round2(settled.ledgerBalance + pack.credits)
      const lines: Entry[] = [
        {
          no: settled.nextNo,
          time,
          what: `Credits · ${card}`,
          amount: pack.credits,
          after: afterCredits,
          status: "Received",
        },
      ]
      if (pack.bonus) {
        lines.unshift({
          no: settled.nextNo + 1,
          time,
          what: `Bonus on ${usd(pack.credits)} pack`,
          amount: pack.bonus,
          after: round2(afterCredits + pack.bonus),
          status: "Bonus",
        })
      }
      const added = pack.credits + pack.bonus
      return {
        ...settled,
        balance: settled.balance + added,
        ledgerBalance: round2(settled.ledgerBalance + added),
        entries: [...lines, ...settled.entries].slice(0, MAX_ENTRIES),
        nextNo: settled.nextNo + lines.length,
        receiptNo: settled.receiptNo + 1,
        receipt: {
          no: `R-0${settled.receiptNo}`,
          time: `17 Oct 2026, ${clockText(settled.clock)}`,
          card,
          lines: pack.bonus
            ? [
                { label: `${pack.credits} credits`, amount: pack.credits },
                { label: `${pack.bonus} bonus credits`, amount: 0 },
              ]
            : [{ label: `${pack.credits} credits`, amount: pack.credits }],
          total: pack.credits,
          stampDate: "17 Oct",
        },
        event: `Paid ${usd(pack.credits)} with ${card}. ${usd(added)} added to the balance.${
          settled.running || settled.balance > 0 ? "" : " Start traffic again when you're ready."
        }`,
      }
    }
  }
}
```

### `registry/bevel/blocks/billing-desk/shared/environment.tsx`

```tsx
import * as React from "react"
import { cn } from "@/lib/utils"

/**
 * Pieces of the physical environments the demos are set in. Each demo
 * mounts Bevel components on a surface; these are the surface's own parts.
 */

/** A legend engraved into the surface: names the control below it. */
export function Legend({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      className={cn(
        "text-[15px] leading-none font-semibold tracking-wide text-(--legend) [text-shadow:0_-1px_0_rgb(0_0_0/0.45),0_1px_0_rgb(255_255_255/0.08)]",
        className
      )}
      {...props}
    />
  )
}

/** A slotted screw head, for panel corners. */
export function Screw({ className, angle = 30 }: { className?: string; angle?: number }) {
  return (
    <span
      aria-hidden
      className={cn("bevel-metal absolute size-3.5 rounded-full shadow-(--bevel-depth-raised)", className)}
    >
      <span className="absolute inset-x-0.5 top-1/2 h-px -translate-y-1/2 bg-black/50" style={{ rotate: `${angle}deg` }} />
    </span>
  )
}

/** Four screws, one in each corner of a positioned panel. */
const screwCorners = {
  "2": ["top-2 left-2", "top-2 right-2", "bottom-2 left-2", "bottom-2 right-2"],
  "3": ["top-3 left-3", "top-3 right-3", "bottom-3 left-3", "bottom-3 right-3"],
  "4": ["top-4 left-4", "top-4 right-4", "bottom-4 left-4", "bottom-4 right-4"],
} as const

export function Screws({ inset = "3" }: { inset?: keyof typeof screwCorners }) {
  return (
    <>
      {screwCorners[inset].map((c, i) => (
        <Screw key={c} className={c} angle={[20, 75, 130, 160][i]} />
      ))}
    </>
  )
}

/** A physical key: a raised plastic button that goes down when pressed. */
export function PanelKey({ className, tone = "plain", ...props }: React.ComponentProps<"button"> & { tone?: "plain" | "go" }) {
  return (
    <button
      type="button"
      data-tone={tone}
      className={cn(
        "bevel-raised inline-flex h-11 items-center whitespace-nowrap justify-center rounded-(--bevel-radius-sm) px-5 text-[15px] font-semibold outline-none select-none",
        "[--bevel-material:#5B636B] text-[#EEF1F3] data-[tone=go]:[--bevel-material:#C9A44A] data-[tone=go]:text-[#2A2208]",
        "transition-[translate,box-shadow] duration-(--bevel-duration-press) active:translate-y-0.5 active:bevel-pressed",
        "focus-visible:ring-3 focus-visible:ring-[#E9D28A]/60 disabled:opacity-50",
        className
      )}
      {...props}
    />
  )
}

/** A backlit status strip: one line of glowing text behind glass. */
export function StatusStrip({ className, children }: { className?: string; children: React.ReactNode }) {
  return (
    <div
      className={cn(
        "relative overflow-hidden rounded-(--bevel-radius-sm) bg-[#0E1A17] px-4 py-3 shadow-(--bevel-depth-recessed)",
        className
      )}
    >
      <p className="text-[17px] sm:truncate tracking-wide text-[#8FF0CF] [text-shadow:0_0_8px_rgb(143_240_207/0.55)]">{children}</p>
      <span aria-hidden className="bevel-glass absolute inset-0" />
    </div>
  )
}
```

### `registry/bevel/blocks/billing-desk/shared/events.ts`

```ts
/**
 * Announces a milestone in a demo (a merge, a top-up) as a DOM event. It
 * does nothing on its own; a page that wants to count milestones listens
 * for "bevel:demo". The Bevel site forwards them to its analytics.
 */
export function demoMilestone(demo: string, action: string) {
  if (typeof window === "undefined") return
  window.dispatchEvent(new CustomEvent("bevel:demo", { detail: { demo, action } }))
}
```

### `registry/bevel/blocks/billing-desk/shared/fonts.ts`

```ts
import { Barlow_Condensed, Libre_Caslon_Text, Overpass, Source_Serif_4 } from "next/font/google"

/** Each demo product has its own type voice, drawn from its world. */

/** Relay: console legends, the condensed grotesk of control-room engraving. */
export const consoleFace = Barlow_Condensed({ subsets: ["latin"], weight: ["500", "600", "700"], variable: "--font-console" })

/** Meterline: a ledger serif, for a desk where money is written down. */
export const ledgerFace = Libre_Caslon_Text({ subsets: ["latin"], weight: ["400", "700"], variable: "--font-ledger" })

/** Shipyard: highway-sign lettering, for a board read from across a room. */
export const signFace = Overpass({ subsets: ["latin"], weight: ["400", "600", "800"], variable: "--font-sign" })

/** Casefile: a document serif, for papers on a desk. */
export const documentFace = Source_Serif_4({ subsets: ["latin"], weight: ["400", "600", "700"], variable: "--font-document" })
```

### `registry/bevel/blocks/billing-desk/page.tsx`

```tsx
import { Meterline } from "@/components/bevel-demos/meterline/meterline"

export default function Page() {
  return <Meterline />
}
```
