# BreathingDot (/docs/charts/breathing-dot)

BreathingDot takes a single level between 0 and 1 and shows how strained a system is right now, as an ambient signal you
can leave running in a corner of the interface. A core dot colored by band, and a ring whose distance from the core is
the level: that is the static frame, and it renders with zero JavaScript. The interactive entry adds the pulse: slow and
small when calm, fast and large when strained, so the state is legible without reading a number. This is a low-precision
ambient read; for an exact figure use `Progress` or `Sparkline`.

```tsx
<BreathingDot value={0.42} title="Load" size={40} />
```

## Install

```tsx
import { BreathingDot } from "@microcharts/react/breathing-dot";

<BreathingDot value={0.42} title="Load" />
```

Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.

## Try it

```tsx
import { BreathingDot } from "@microcharts/react/breathing-dot/interactive";

<BreathingDot
  value={0.42}
/>
```

## Motion, and reduced motion

The pulse **is** the encoding: its rate and amplitude snap to three named bands (calm, elevated, strained). Three,
because ambient motion supports about three discriminable states, and more would be false precision. This is the
deliberate exception to the no-looping-animation rule, allowed because the loop parameter (the rate) is the datum. The
animation is gated twice. It pauses when the dot scrolls off-screen (a shared observer), and it never runs at all under
`prefers-reduced-motion`; reduced-motion readers get exactly the static frame, where the ring's distance from the core
already carries the level. The band is announced through a polite live region only when it changes, never on every
pulse.

## When to use it

Use it for an ambient "how strained is it right now" read, a live status dot in a header or KPI card, or per-node load
in a dense table. For an exact load figure use `Progress` or `Sparkline`; for a trend over time, `Sparkline`. The
boundary with `HeartbeatBlip` is the data shape: a continuous level is a BreathingDot, discrete arriving events are a
HeartbeatBlip.

## Sizing

One `size` prop, in viewBox units, sets the glyph box — it defaults to `16`, about the height of a line of body text.
There is no separate `width`/`height`: the dot is square, and the rendered box widens only by the gutter a `label`
reserves. `fontSize` follows `size` unless you set it.

## Variants

```tsx
<BreathingDot value={0.72} thresholds={[0.6, 0.85]} />
<BreathingDot value={0.42} label="value" />
```

## Edge cases

```tsx
<BreathingDot value={null} />
```

```tsx
<BreathingDot value={1.4} label="value" />
```

```tsx
<BreathingDot value={Number.NaN} />
```

```tsx
<BreathingDot value={0.723} label="value" locale="de-DE" />
```

An unknown value (`null` or `NaN`) turns the dot gray, drops its ring, and **never pulses**, so an unknown system never
renders as a calm one.

`format`/`locale` reach the `label="value"` percent numeral — always a whole percent, so a locale mostly changes digit
shapes here, not grouping or decimals. The band words in the accessible summary ("calm" / "elevated" / "strained") stay
in `strings` (`BreathingDotStrings`), not the number formatter.

## Four homes

**In a sentence**

```tsx
<p>
  System load right now{" "}
  <span className="mc-inline">
    <BreathingDot value={0.42} summary={false} />
  </span>{" "}
  — moderate strain, pulsing at 42%.
</p>
```

**In a table cell**

```tsx
<td>
  <BreathingDot value={0.42} />
</td>
```

**In a KPI card**

```tsx
<div className="kpi">
  <span className="figure">42%</span>
  <span className="unit">current strain</span>
  <BreathingDot value={0.42} />
</div>
```

**In a tab header**

```tsx
<button className="tab">
  api <BreathingDot value={0.42} />
</button>
```

## Accessibility

The accessible name is the level and its band word — **"Load 20% — calm."** — or **"Load unknown."** when the value is
missing. The band color is never the only signal (ring offset and pulse rate double it), motion is fully gated on
`prefers-reduced-motion`, and the live region announces band changes only.

This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).
Hover or focus also reveals the reading itself in a floating chip, for the sizes and label modes where the mark
does not print it; `readout={false}` drops the chip and keeps everything else.

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `value` (required) | `number` | Level 0–1 (clamped). null / NaN → unknown. |
| `thresholds` | `[number, number]` | calm / elevated / strained edges (default [0.5, 0.8]). |
| `label` | `"value" \| "none"` | Percent numeral beside the dot. |
| `size` | `number` | Dot box edge in viewBox units (default 16). |
| `fontSize` | `number` | Type size of the percent numeral, in viewBox units. Defaults from `size`. |

Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
