# Dial (goodtabs UI)

A rotary slider that turns the pointer angle into a snapped value.

## Install

```bash
npx shadcn@latest add https://ui.goodtabs.co/r/dial.json
```

Or, with the @goodtabs namespace configured in components.json: `npx shadcn@latest add @goodtabs/dial`

## Usage

```tsx
import { Dial } from "@/components/ui/dial"

<Dial label="Volume" defaultValue={60} step={5} />
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| label | `string` |  | Accessible name. |
| value | `number` |  | Controlled value. |
| defaultValue | `number` | `0` | Starting value when uncontrolled. |
| onValueChange | `(value: number) => void` |  | Called with the snapped value. |
| min | `number` | `0` | Lowest value. |
| max | `number` | `100` | Highest value. |
| step | `number` | `5` | Snap size for pointer and keys. |
| size | `number` | `96` | Diameter in px. |

## When to use

- Tuning a single value where the rotary gesture fits, like volume or hue
- Audio, camera and design tool controls

## When not to use

- Forms where a plain slider or number field is faster
- Values that need exact entry, pair it with an input

## Notes for agents

- Client component with the real slider role. A demo that passes a function must be a client component
- Arrows step, Page keys move by ten steps, Home and End jump to the ends
- The sweep is 270 degrees with the gap at the bottom. Dragging past an end clamps to it
- Pointer capture keeps dragging alive outside the dial; touch-action is none on the dial only

## The maths

- Pointer angle θ = atan2(dx, -dy) in degrees, clockwise from the top.
- The value is min + ((θ + 135) / 270) × (max - min), then snapped to the step.
- The arc is 270 / 360 = 0.75 of the circumference 2πr, drawn with dasharray 0.75 × 2πr × p.

Docs: https://ui.goodtabs.co/components/dial