# Value Display

> An audio value as it reads, in steady monospace figures, for tempo, level, frequency, pan, percent, semitones and time.

```tsx
import {
  ValueDisplay,
  type ValueKind,
} from "@/components/audio-ui/value-display";

const VALUES: { kind: ValueKind; label: string; value: number }[] = [
  { kind: "bpm", label: "Tempo", value: 128 },
  { kind: "db", label: "Gain", value: -6 },
  { kind: "hz", label: "Cutoff", value: 2400 },
  { kind: "pan", label: "Pan", value: -0.3 },
  { kind: "percent", label: "Mix", value: 0.35 },
  { kind: "semitones", label: "Transpose", value: 7 },
  { kind: "time", label: "Position", value: 62.345 },
];

export function ValueDisplayDemo() {
  return (
    <dl className="grid w-full max-w-xs grid-cols-[auto_1fr] items-baseline gap-x-6 gap-y-1.5">
      {VALUES.map((item) => (
        <div className="contents" key={item.kind}>
          <dt className="text-muted-foreground text-xs uppercase tracking-wide">
            {item.label}
          </dt>
          <dd className="text-end">
            <ValueDisplay
              kind={item.kind}
              label={item.label}
              value={item.value}
            />
          </dd>
        </div>
      ))}
    </dl>
  );
}
```

An audio value as a DAW shows it, in monospace figures that do not jump as they change: `120.00 BPM`, `-6.0 dB`, `1.2 kHz`, `L 30`, `50 %`, `+3 st`, `1:02.345`.

## Installation

**CLI**

```bash
npx shadcn@latest add @audio-ui/value-display
```

**Manual**

**1. Install the following dependencies:**

```bash
npm install @audio-ui/core class-variance-authority
```

**2. Copy and paste the following code into your project.**

```tsx title="components/audio-ui/value-display.tsx"
import { formatDb, formatHz } from "@audio-ui/core/analysis";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

/** What a value is, which says how it reads. */
export type ValueKind =
  | "bpm"
  | "db"
  | "hz"
  | "pan"
  | "percent"
  | "semitones"
  | "time";

const SIGNED = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  signDisplay: "exceptZero",
});

/** Minutes, seconds and milliseconds: 1:02.345. */
function formatTime(seconds: number) {
  const whole = Math.max(0, seconds);
  const minutes = Math.floor(whole / 60);
  const rest = whole - minutes * 60;
  return `${minutes}:${rest.toFixed(3).padStart(6, "0")}`;
}

/** A pan from -1 to 1: C in the middle, L or R and how far, in percent. */
function formatPan(pan: number) {
  const amount = Math.round(Math.abs(pan) * 100);
  if (amount === 0) {
    return "C";
  }
  return `${pan < 0 ? "L" : "R"} ${amount}`;
}

/** How a value of each kind reads: as a DAW shows it. */
export function formatValue(value: number, kind: ValueKind): string {
  switch (kind) {
    case "bpm":
      return `${value.toFixed(2)} BPM`;
    case "db":
      return formatDb(value);
    case "hz":
      return formatHz(value);
    case "pan":
      return formatPan(value);
    case "percent":
      return `${Math.round(value * 100)} %`;
    case "semitones":
      return `${SIGNED.format(value)} st`;
    default:
      return formatTime(value);
  }
}

const valueDisplayVariants = cva("font-mono tabular-nums tracking-tight inline-block", {
  defaultVariants: { size: "default" },
  variants: {
    /** "lg" for the main value of a panel. */
    size: {
      default: "text-sm",
      lg: "font-medium text-2xl",
      sm: "text-xs",
    },
  },
});

export interface ValueDisplayProps
  extends VariantProps<typeof valueDisplayVariants> {
  className?: string;
  /** How it reads: a tempo, a level, a frequency, a pan, a share, a pitch, a time. */
  kind: ValueKind;
  /** Names it for assistive technology, which reads it with its value: "Cutoff". */
  label?: string;
  value: number;
}

/**
 * A value as it reads, in monospace figures that do not jump as they change:
 * 120.00 BPM, -6.0 dB, 1.2 kHz, L 30, 50 %, +3 st, 1:02.345. `formatValue`
 * gives the same text, for a title or an announcement.
 */
export function ValueDisplay({
  className,
  kind,
  label,
  size,
  value,
}: ValueDisplayProps) {
  const text = formatValue(value, kind);
  return (
    <output
      aria-label={label ? `${label}: ${text}` : undefined}
      className={cn(valueDisplayVariants({ size }), className)}
      data-kind={kind}
      data-size={size ?? "default"}
      data-slot="value-display"
    >
      {text}
    </output>
  );
}
```

**3. Update the import paths to match your project setup.**

**Agent**

Give your agent the skill, and it picks the component, installs it and wires it to your audio:

```bash
npx skills add ouestlabs/audio-ui
```

Then ask it:

```text
Add the value display from audio/ui to my project and wire it to my audio code.
```

To let the agent read the docs live, connect the [MCP server](/docs/mcp). More in [Agent Skill](/docs/agents).

## Usage

```tsx
import { formatValue, ValueDisplay } from "@/components/audio-ui/value-display";

<ValueDisplay kind="db" label="Gain" value={gain} />

// The same text, for a title or an announcement.
formatValue(gain, "db");
```

| `kind` | Value | Reads |
| --- | --- | --- |
| `bpm` | beats per minute | `120.00 BPM` |
| `db` | decibels, `-Infinity` for silence | `-6.0 dB`, `-∞ dB` |
| `hz` | hertz | `440 Hz`, `1.2 kHz` |
| `pan` | -1 to 1 | `L 30`, `C`, `R 100` |
| `percent` | 0 to 1 | `50 %` |
| `semitones` | semitones | `+3 st` |
| `time` | seconds | `1:02.345` |

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` |  | The value. |
| `kind` | `ValueKind` |  | How it reads: a tempo, a level, a frequency, a pan, a share, a pitch, a time. |
| `label` | `string` |  | Names it for assistive technology, which reads it with its value: "Cutoff". |
| `size` | `"sm" \| "default" \| "lg"` | `"default"` | "lg" for the main value of a panel. |
