# Payments Globe

> A dotted globe you can spin, with payments flying home from their cities as arcs, a rolling total, a top-cities list to pick from, a day you can replay, and a live mode.

- Name: `payments-globe`
- Status: beta
- Categories: dashboard, revenue, payments, map, globe, live
- Files: the install writes `payments-globe.tsx`, and any part files named `payments-globe-*.tsx`, to your `components` alias, `@/components` by default. Edit them freely. They are yours.
- npm packages: `lucide-react`. The install adds them.
- Other registry items: `@txbrendan/badge`, `@txbrendan/button`, `@txbrendan/card`, `@txbrendan/motion-timing`, `@txbrendan/notice`, `@txbrendan/rolling-number`, `@txbrendan/skeleton`, `@txbrendan/slider`. The install adds them.
- Gallery: https://txbrendan.dev/blocks/payments-globe
- Registry item: https://txbrendan.dev/r/payments-globe.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@txbrendan": "https://txbrendan.dev/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@txbrendan` in a public list that txbrendan/ui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @txbrendan/theme
   ```

3. Add the block. It also adds every component it uses:

   ```bash
   npx shadcn@latest add @txbrendan/payments-globe
   ```

## Use it for

- The hero of a revenue or sales dashboard, where seeing money arrive from around the world is the point.
- Pass one home city, the cities payments come from, and each payment's city, time, and amount. The block works out every total.
- For live mode, set live and add each new payment to payments as it arrives. The block never fetches anything itself.
- Pass now from your server when the page must render the same on the server and in the browser, like a demo or a test.
- Set timeZone to where your day starts. The clock, the hours, and the day all follow it.
- Pass format as number format options, like { style: "currency", currency: "EUR" }, so a server component can render the block.

## Avoid it for

- Exact places, like store locations or delivery routes. The land is drawn by hand, so it is rough. Use a real map.
- A plain total, or a total over time. Use Stats Cards or a chart. They are smaller and easier to read.
- More than about 60 cities. Dots crowd together and the list gets long. Group small places into their nearest big city.

## Props

- `home`: `{ name: string; lat: number; lon: number }`. The city every payment flies to, like your headquarters. The globe starts facing it.
- `cities`: `{ id: string; name: string; lat: number; lon: number }[]`. Where payments come from. Ids must be unique. Cities with no payments that day are left off.
- `payments`: `{ cityId: string; at: number; amount: number }[]`. Each payment: its city's id, its time in ms, and its amount. Any order. Payments outside the day are ignored.
- `format`: `Intl.NumberFormatOptions | ((value: number) => string)`. Default `{ style: "currency", currency: "USD", maximumFractionDigits: 0 }`. How amounts print. Options work from a server component. A function works from a client component only.
- `locale`: `string`. Default `"en-US"`. The locale for format options.
- `timeZone`: `string`. Default `"UTC"`. Where the day starts and how the clock reads, like "America/New\_York".
- `live`: `boolean`. Default `false`. Follows the current time and shows a Live badge. Moving the scrubber leaves it until Back to live.
- `now`: `number`. The current time in ms. When it is not set, the block reads the clock after it mounts.
- `day`: `number`. Any time on the day to show. Default the day of now, or of the newest payment.
- `selectedCityId`: `string | null`. The picked city's id. Leave it out to let the block keep track.
- `onSelectedCityChange`: `(id: string | null) => void`. Called with a city's id when someone picks it, or null when they clear it.
- `title`: `string`. Default `"Revenue today"`. The label over the total. A picked city replaces it with its own name.
- `loading`: `boolean`. Default `false`. Shows placeholders at the same size while the data loads.
- `loadingText`: `string`. Default `"Loading payments"`. What the status line tells screen readers while loading.
- `error`: `ReactNode`. Shows this message in place of the payments. The globe stays, with no arcs.
- `emptyText`: `ReactNode`. Default `"No payments yet today."`. Shown when the day has no payments.
- `visibleCities`: `number`. Default `5`. How many cities the list shows before Show all.
- `aria-label`: `string`. Default `"Payments by city"`. Names the globe for screen readers. The block adds how many cities and the total so far.
- `className`: `string`. Extra classes on the outer div, which holds the container query.

## Accessibility

- The globe is a focusable group named by aria-label, with the number of cities and the total so far. Arrow keys turn it, and Home turns it back to the home city.
- Turn west and Turn east buttons turn the globe too, so nobody has to drag. A tap on a city dot picks it, but the list is the main way.
- A touch drag only spins the globe sideways. Dragging up and down still scrolls the page.
- The city list carries the real numbers. Each city is a button with aria-pressed. Picking one turns the globe to it and shows its own total.
- At rest the globe sways slowly so home stays in sight. Pause motion stops the sway and the arcs. The motion feel turns them off on its own at 0ms, and the arcs then show still.
- A status line speaks when someone picks a city, or a replay starts or ends. It never speaks for each payment.
- The scrubber's name reads the time of day, like "Time of day 16:42:14 UTC". Its value is in minutes.
- Arcs, dots, and the home city keep system colors in forced-colors mode.

## Examples

### Today's payments

```tsx
<PaymentsGlobe
  home={{ name: "San Francisco", lat: 37.77, lon: -122.42 }}
  cities={[
    { id: "nyc", name: "New York", lat: 40.71, lon: -74.01 },
    { id: "lon", name: "London", lat: 51.51, lon: -0.13 },
    { id: "tyo", name: "Tokyo", lat: 35.68, lon: 139.69 },
  ]}
  payments={payments}
/>
```

### Live, in your own time zone

```tsx
<PaymentsGlobe home={home} cities={cities} payments={payments} live timeZone="America/Los_Angeles" />
```

### In euros

```tsx
<PaymentsGlobe home={home} cities={cities} payments={payments} format={{ style: "currency", currency: "EUR", maximumFractionDigits: 0 }} locale="de-DE" />
```

### While the payments load

```tsx
<PaymentsGlobe home={home} cities={[]} payments={[]} loading />
```
