# Usage Meter

> One number for a whole, split into parts with a legend, like 412 GB of 1 TB or $84K of a $100K goal. Its digits roll and its parts slide when values change. Against a limit it warns, and against a goal it turns success.

- Name: `usage-meter`
- Status: beta
- Categories: data, display
- File: the install writes `usage-meter.tsx` to your `ui` alias, `@/components/ui` by default.
- Other registry items: `@arbokui/badge`, `@arbokui/button`, `@arbokui/card`, `@arbokui/notice`, `@arbokui/rolling-number`, `@arbokui/skeleton`. The install adds them.
- Gallery: https://arbokui.com/components/usage-meter
- Registry item: https://arbokui.com/r/usage-meter.json

## Install

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

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

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

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui 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 @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/usage-meter
   ```

## Use it for

- Show how much of a limit is used, like storage, a budget, seats, or an API quota, and what fills it.
- Show progress to a goal, like sales or money raised, split by where it came from. Pass goal.
- Show how a total splits with no limit, like visits by source. Leave max out.
- Pass parts in the order the bar shows them. Keep to five. Each takes the next chart color. Group small ones into Other.
- Pass unit for a word after each number, like "GB" or "seats". Pass valueFormat for money or percents, like { style: "currency", currency: "USD" }.
- A limit turns warning at 80% of max and danger once full. Pass warnAt to warn earlier, like 0.6. A goal never warns and turns success once reached.
- Pass the parts again with new values, and the digits roll and the bar slides.
- Give a part its own action, like { label: "Clean up" } on Photos. Pass action for the card's next step, like an Upgrade button.
- Pass legend={false} and size="sm" for a small space.

## Avoid it for

- A task that runs, like an upload. Use Progress.
- One plain share with no parts and no card, like 72% done. Use Progress or Ring Progress.
- Many items compared side by side, or a ranking. Use Breakdown List.
- A total over time. Use a chart, like Area Chart.
- Tokens in a model's context. Use Context Window.

## Props

- `parts`: `{ id?: string; label: string; value: number; action?: UsageMeterPartAction }[]`. Required. What makes up the total, in the order the bar shows them. Five at most.
- `UsageMeterPartAction`: `{ label: string; onClick?: () => void; loading?: boolean; disabled?: boolean }`. Type. A quiet button at the end of the part's legend row. Pass functions from a client component.
- `max`: `number`. The limit or the goal, like 1000. Left out, the bar splits the total, with no percent and no warning.
- `goal`: `boolean`. Default `false`. max is a goal to reach, not a limit. It never warns, says how much is to go, and turns success once reached.
- `title`: `ReactNode`. Default `"Usage"`. The card's title. It names the meter.
- `badge`: `ReactNode`. A small tag beside the title, like the plan's name.
- `unit`: `string`. A word after each number, like "GB" or "seats". Leave it out when valueFormat prints the unit.
- `valueFormat`: `Intl.NumberFormatOptions`. Default `{ notation: "compact", maximumFractionDigits: 1 }`. Formats every number. Merged over the default, so { style: "currency", currency: "USD" } prints $84K. Pass { notation: "standard" } for full numbers.
- `warnAt`: `number`. Default `0.8`. For a limit, the share of max, from 0 to 1, where it turns warning. It turns danger once full.
- `tone`: `"neutral" | "brand" | "danger" | "success" | "warning"`. Default `"neutral"`. Colors the percent and the last line. Left out, it follows the share of max. Passed, it stays that tone.
- `legend`: `boolean`. Default `true`. Shows each part's name, value, and action under the bar.
- `action`: `ReactNode`. A button in the last row, like Upgrade or Raise limit.
- `loading`: `boolean`. Default `false`. Shows placeholders while the numbers load.
- `size`: `"sm" | "md"`. Default `"md"`. Bar height and the number's size.
- `locale`: `string`. Default `"en-US"`. The locale for valueFormat.
- `className`: `string`. Classes for the card. Later classes win.

## Accessibility

- The card is a group named by the title. With max, the bar is a meter with the same name, and reads like "412 GB of 1,000 GB, 41%".
- With no max, the bar is hidden from screen readers, and the legend says each part in words.
- The bar's colored parts are hidden from screen readers. The legend says each part's name and value in text.
- The big number's rolling digits are hidden. Screen readers hear the final value only.
- A part's action is named with the part, like "Clean up Photos", so each button says what it acts on.
- The last line says Almost full, Full, or Goal reached in words, so the state does not rest on color alone.
- The bar slides and the digits roll on the motion variables, so reduced motion jumps straight to the new values.
- In forced-colors mode the bar draws a border and its parts fill with the text color.

## Examples

### Storage

```tsx
<UsageMeter
  title="Storage"
  badge="Pro plan"
  max={1000}
  unit="GB"
  valueFormat={{ notation: "standard" }}
  parts={[
    { label: "Photos", value: 212 },
    { label: "Videos", value: 128 },
    { label: "Documents", value: 46 },
    { label: "Other", value: 26 },
  ]}
/>
```

### A budget near its limit

```tsx
import { Button } from "@/components/ui/button";
import { UsageMeter } from "@/components/ui/usage-meter";

export function MarketingBudget() {
  return (
    <UsageMeter
      title="Q3 budget"
      max={50000}
      valueFormat={{ style: "currency", currency: "USD" }}
      parts={[
        { label: "Ads", value: 26400 },
        { label: "Events", value: 12100 },
        { label: "Tools", value: 4300 },
      ]}
      action={
        <Button size="sm" emphasis="outline">
          Raise limit
        </Button>
      }
    />
  );
}
```

### A goal

```tsx
<UsageMeter
  title="Spring fundraiser"
  goal
  max={100000}
  valueFormat={{ style: "currency", currency: "USD" }}
  parts={[
    { label: "Online", value: 48200 },
    { label: "Events", value: 21500 },
    { label: "Sponsors", value: 14000 },
  ]}
/>
```

### A total with no limit

```tsx
<UsageMeter
  title="Visits this week"
  unit="visits"
  parts={[
    { label: "Search", value: 6400 },
    { label: "Direct", value: 3100 },
    { label: "Social", value: 1900 },
    { label: "Email", value: 1000 },
  ]}
/>
```
