# Storage

> A storage meter with a raccoon and his suitcase: near the limit the case bulges and Pack sits on it to shut it; clean up and he hops down, upgrade and a roomy trunk arrives.

Storage limits usually show up as a thin bar turning red. This one packs a suitcase. A teal case stands shut on the lit stage and Pack, an illustrated raccoon, leans on it at his ease. From 90% of the limit the case bulges, a sock and a holiday photo poking out of the zip, and Pack sits on top pushing down with all he’s got, wobbling as the case gives under him. Empty the trash and the case snaps shut again and he hops back down; upgrade and a roomy leather trunk takes its place while he cheers. Under the stage is the real meter: what’s used of the limit with every number rolling, one bar split by what the space is used by, a legend with the free space, and Clean up and Upgrade wired to your functions.

## Install

```bash
npx shadcn@latest add https://hairlineui.com/r/storage.json
```

Needs React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

- npm: none
- Hairline UI components it needs (the CLI installs them too): `number-roll`, `status-button`, `text-morph`
- Docs and live preview: https://hairlineui.com/ui/storage
- Registry item (source): https://hairlineui.com/r/storage.json

## Usage

```tsx
import { Storage } from "@/components/storage";

<Storage
  used={usage.bytes}
  limit={plan.bytes}
  kinds={[{ id: "photos", label: "Photos", bytes: usage.photos }]}
/>
```

## With clean-up and upgrade

Both actions call your API and then refresh the numbers; roomy is your call for plans with plenty of space.

```tsx
"use client";
import useSWR from "swr";
import { Storage } from "@/components/storage";

export function StorageCard() {
  const { data, mutate } = useSWR("/api/storage", (u) => fetch(u).then((r) => r.json()));
  if (!data) return null;
  return (
    <Storage
      used={data.used}
      limit={data.limit}
      roomy={data.limit >= 1e12}
      kinds={data.kinds} // [{ id, label, bytes }]
      cleanUpLabel={`Empty trash · ${(data.trash / 1e9).toFixed(1)} GB`}
      onCleanUp={async () => {
        await fetch("/api/storage/trash", { method: "DELETE" });
        await mutate();
      }}
      upgradeLabel="Upgrade to 1 TB"
      onUpgrade={async () => {
        window.location.href = "/billing/upgrade?plan=1tb";
      }}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `used / limit` | `number` | – | Bytes used and bytes allowed. |
| `kinds` | `StorageKind[]` | `[]` | { id, label, bytes, color? } for the bar and its legend; colours default to --chart-1…5. |
| `onCleanUp / cleanUpLabel` | `() => Promise<unknown> / string` | `— / "Clean up"` | Free some space; update used afterwards. Throw to show Try again. |
| `onUpgrade / upgradeLabel` | `() => Promise<unknown> / string` | `— / "Upgrade"` | Move to a bigger plan; update limit (and roomy) afterwards. |
| `roomy` | `boolean` | `false` | Show the roomy trunk and hide Upgrade. |
| `name` | `string` | `"Pack"` | What the raccoon is called in his lines. |

## States

- **roomy**: Under 75%: the case stands shut and Pack leans on it; “Plenty of room”.
- **snug**: 75% to 90%: the same, with “Getting snug in there”.
- **full**: From 90%: the bulging case swaps in (poses pop in from a 2px blur with a slight overshoot) and Pack sits on top, wobbling every 520ms as the case squashes under him; the used figure turns amber; “Pack can’t get it shut”.
- **clean up**: Clean up → Cleaning up → Cleaned up; when used drops under 90% the case shuts and he hops down; the bar and the sizes roll down.
- **upgrade**: Upgrade → Upgrading → Upgraded; with roomy the trunk takes the case’s place and Pack cheers beside it; “Room to spare”.

## What it handles

A usage bar alone doesn’t say what to do about it. This block shows what the space is used by, makes being nearly full feel like it, and puts the two ways out side by side.

- **Nearly full, plainly**: From 90% the case bulges, Pack sits on it and the numbers turn amber.
- **What it’s used by**: kinds split one bar by type with a legend and the free space.
- **Numbers that roll**: Used, limit and every size roll to their new values, in B, KB, MB, GB or TB.
- **Two ways out**: onCleanUp and onUpgrade sit side by side with Pending, Done and Try again states.
- **Room to spare**: roomy swaps in the trunk for plans with plenty of space.
- **A real meter**: The bar is a meter with the used and allowed bytes and a readable value.

## Replaces

a usage meter, a breakdown bar and legend, byte formatting, clean-up and upgrade buttons

## Where it fits

- **Storage settings**: The usage card in a file, photo or backup app.
- **Plan limits**: Any metered limit near its cap, with the upgrade right there.
- **Clean-up prompts**: Empty the trash or drop old versions before paying for more.
- **Billing pages**: Usage beside the plan, so the upgrade makes sense.

## Notes

- The bar is a meter with aria-valuenow, aria-valuemax and a readable value text; the legend lists every kind with its size and the free space.
- Pack is decoration (aria-hidden). The cases and his three poses are illustrations shipped inside pack-art.ts as WebP data URIs (50KB of images), so the install is complete; the swaps, the pop and the wobble are code.
- The wobble is a compositor animation that only runs while he’s sitting on a full case.
- With reduced motion the poses swap in place and nothing wobbles.
- Built from Number roll, Status button and Text morph, which the CLI installs with it. No animation library.

License: MIT (https://hairlineui.com/license#components)
