# Vitals Card

> An animated vitals card for React and shadcn, with live readings that warn outside their usual range.

Live vitals that read like a bedside monitor. Each metric is a tab whose value rolls; the selected one scrolls continuously over its usual range, and leaving the range turns the line, value and flag warning. Heart rate's glyph beats at the rate.

- Tier: Eased Pro
- Interaction: Loading
- Page: https://eased.dev/components/vitals-card
- Installs: motion, d3-shape

## Install

Vitals Card ships with Eased Pro (https://eased.dev/#pricing). Members add the `@eased-pro` registry with their token (https://eased.dev/account), then run:

```bash
npx shadcn add @eased-pro/vitals-card
```

Live vitals that read like a bedside monitor: metric tabs whose values roll, and a line that scrolls continuously over the usual range. Built on Number Roll, the chart palette and realtime-core's ticker.

## When to use

- Remote monitoring or a wearable companion: heart rate, oxygen and breathing arriving every second or so.
- When a value leaving its usual range should be seen at a glance, without an alarm.
- When the last minute matters more than the last month; for long trends use a Line Chart.

## When not to use

- Historical trends over days or weeks: use [Line Chart](/components/line-chart) or [Metrics Dashboard](/components/metrics-dashboard).
- A single static reading: use [Stat Tile](/components/stat-tile).
- Clinical alarms. This block informs; it does not alert, escalate or diagnose.

## Usage

```tsx example.tsx
import { useMemo, useRef } from "react";
import { createTickStream, useTicker } from "@/components/eased/_primitives/realtime-core/realtime-core";
import { VitalsCard } from "@/components/eased/vitals-card/vitals-card";

export function Monitor() {
  const ref = useRef<HTMLDivElement>(null);
  const stream = useMemo(() => createTickStream({ seed: "vitals", series: [{ id: "hr", initial: 72, kind: "level", decimals: 0 }] }), []);
  const live = useTicker(stream, { target: ref, history: 61 });
  return (
    <div ref={ref} className="flex h-96 flex-col">
      <VitalsCard
        className="flex-1"
        tick={live.index}
        paused={!live.running}
        metrics={[{ id: "hr", label: "Heart rate", unit: "bpm", values: live.series("hr"), range: { low: 50, high: 100 }, domain: [40, 130], pulse: true }]}
      />
    </div>
  );
}
```

Pass one more sample than the window you want to show: the extra one waits past the edge and slides in.

## API reference

### VitalsCard

| Prop | Type | Default | Description |
|---|---|---|---|
| `metrics` | `VitalMetric[]` | | `{ id, label, unit, values, range, domain?, decimals?, color?, pulse? }`. `values` are evenly spaced, oldest first. |
| `tick` | `number` | | Increments with each new sample; each change scrolls the line one step. |
| `intervalMs` | `number` | `1000` | Time between samples; one scroll step lasts this long. |
| `paused` | `boolean` | `false` | Stops the scroll and the heartbeat. Pass `!ticker.running`. |
| `value` / `defaultValue` / `onValueChange` | `string` | first metric | The selected metric. |
| `windowLabel` | `string` | `"Last 60 seconds"` | Above the chart and in its accessible name. |
| `locale` | `string` | `"en-US"` | |
| `className` | `string` | `""` | Appended to the root. Give it a height; the chart fills it. |

## Keyboard

| Key | Action |
|---|---|
| `Tab` | Moves to the selected metric tab. |
| `←` / `→`, `Home` / `End` | Move between metrics and select one. |

## Accessibility

- The metrics are a `tablist`; the chart is the `tabpanel`, an image named with the current value and the usual range.
- Out-of-range values are marked by colour and by the flag's text, never colour alone.
- The heartbeat and the scroll stop off screen, while paused, and under reduced motion.

## Motion

The line is redrawn once per sample and slid one step left over the sample interval with a linear tween, so it moves continuously rather than in jumps. Values roll on the snappy spring. Leaving the range tints the stretch outside the band, the newest dot and the value in warning over 200ms, and the flag pops in from 85%. The heart beats at the live rate. Reduced motion: no scroll or heartbeat; values change at once.

## Notes for AI

Pick this for live, per-second vitals with a usual range. Feed it from `useTicker` in realtime-core (deterministic, pauses off screen) and pass `tick={ticker.index}` and `paused={!ticker.running}`. Set `domain` wider than `range` so excursions have room, and `pulse` only on heart rate.

Don't animate the samples yourself or remount on each tick; the block scrolls from a motion value. Don't use `danger` colours or alarm copy for out-of-range values, and don't present the readout as a diagnosis.

## Related

- [Lab Results](/components/lab-results): reference ranges for slower, discrete results.
- [Sparkline](/components/sparkline): a word-sized trend.
