# useMediaControls

> A React hook for custom audio and video players: play, seek, volume, speed, buffering and a smooth playhead from one media element.

- Type: React hook (Hooks & Utilities)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add use-media-controls`
- Docs: https://wingo-ui.com/components/use-media-controls

## Interaction

Press play on the podcast: the bar glides every frame while the time text changes once a second. Tap or drag on the bar to seek, change the speed and mute. With the player focused, Space or k plays and pauses, Left / Right seek by the step, j / l by twice that, Up / Down change the volume, m mutes, 0 to 9 jump to 0% to 90%, < and > change the speed. Turn exclusive on and starting one voice message pauses the other and the podcast.

## Installation

```bash
npx wingo-ui@latest add use-media-controls
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/use-media-controls` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `hooks/use-media-controls.ts`

npm dependencies: `npm i motion`

## Usage

```tsx
import { motion } from "motion/react"
import { formatMediaTime, handleMediaKey, useMediaControls } from "@/hooks/use-media-controls"

export function VoiceNote({ src }: { src: string }) {
  // destructure ref: reading media.ref for the ref prop makes the react compiler treat all of media as a ref
  const { ref, ...media } = useMediaControls({ exclusive: true })
  return (
    <div tabIndex={0} onKeyDown={(event) => handleMediaKey(event, media)} className="flex items-center gap-3">
      <audio ref={ref} src={src} preload="metadata" />
      <button type="button" onClick={media.toggle}>{media.playing ? "Pause" : "Play"}</button>
      <div className="h-1 flex-1 overflow-hidden rounded-full bg-foreground/10">
        <motion.div className="h-full origin-left bg-foreground" style={{ scaleX: media.progress }} />
      </div>
      <span className="tabular-nums">{formatMediaTime(media.playing ? media.second : media.duration)}</span>
    </div>
  )
}
```

## API

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `exclusive?` | `boolean` | `false` | Starting this player pauses every other exclusive player on the page (voice messages in a chat, a list of episodes). |
| `defaultRate?` | `number` | `1` | The playback speed applied when the element attaches (defaultVolume 0 to 1 and defaultMuted work the same way). |
| `seekStep?` | `number` | `5` | handleMediaKey(event, controls, { seekStep }): seconds for Left / Right; j / l seek twice as far. |
| `defaultVolume / defaultMuted?` | `number / boolean` | `1` | Applied when the element attaches. |
| `onPlay / onPause / onEnded / onError / onTimeUpdate?` | `() => void / (error: MediaError \| null) => void / (seconds: number) => void` |  | Lifecycle callbacks; onTimeUpdate fires with whole seconds (resume positions, analytics). The latest callbacks are always used. |
| `useMediaControls?` | `(options?) => MediaControls` |  | Returns { ref (put it on the <audio> / <video>; destructure it, const { ref, ...media } = useMediaControls(), because the React compiler lint treats an object whose field feeds a ref prop as a ref), element, playing, waiting (buffering), ended, ready, error, duration, second (whole seconds as state, re-renders at most once a second), currentTime / progress / buffered (motion values, progress and buffered 0 to 1, updated every frame while playing), volume, muted, rate, play() (resolves false when the browser blocks it), pause(), toggle(), seek(seconds), seekBy(delta), seekToFraction(0..1), setVolume, setMuted, toggleMute, setRate }. |

## Parts

### handleMediaKey (function)

The shared keyboard map for onKeyDown on a player root. Leaves sliders, inputs, menus and listboxes alone; returns true (and prevents the default) when it handled the key. Signature: (event, controls, options?: { seekStep?: number = 5; volumeStep?: number = 0.1; rates?: number[]; volumeKeys?: boolean = true }) => boolean.

### formatMediaTime (function)

"0:42", "12:05", "1:02:03"; unknown or live durations print "--:--". forceHours keeps the text width stable in long videos. Signature: (seconds: number, options?: { forceHours?: boolean }) => string.

### MediaControls (type)

The return value: ref, element, playback flags, duration, second, motion values for the playhead and every action.

### UseMediaControlsOptions (type)

The options object: exclusive, defaultRate, defaultVolume, defaultMuted and the event callbacks.

### MediaKeyOptions (type)

The options of handleMediaKey: seekStep, volumeStep, rates and volumeKeys.

### MEDIA_RATES (constant)

0.5, 0.75, 1, 1.25, 1.5, 1.75, 2: the speed menu of every player. Type: readonly number[].

## Examples

- **Voice messages, one at a time**: exclusive: starting one voice message pauses the other, like a chat app.
- **Podcast at 1.5x**: defaultRate 1.5 and a 15 second seek step for the arrow keys.
- **Custom video controls**: The hook on a <video> element, with a play button and a gliding progress bar over the picture.

## See also

- [Audio Player](https://wingo-ui.com/components/audio-player) ([Markdown](https://wingo-ui.com/components/audio-player.md))
- [Video Player](https://wingo-ui.com/components/video-player) ([Markdown](https://wingo-ui.com/components/video-player.md))
- [Voice Input](https://wingo-ui.com/components/voice-input) ([Markdown](https://wingo-ui.com/components/voice-input.md))
- [Slider](https://wingo-ui.com/components/slider) ([Markdown](https://wingo-ui.com/components/slider.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))

---

Canonical: https://wingo-ui.com/components/use-media-controls
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
