# iOS Music Player

> Album art, scrubber, transport controls and auto-advance.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/templates/ios-music-player
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/ios-music-player.json
```

Adds the page at `app/ios-music-player/page.tsx` together with the components below. Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Components

- [iOS Kit](https://chromotion.dev/r/ios-kit.md): `@/components/ios-kit`

## Dependencies

```bash
npm install lucide-react
```

## Source

```tsx title="app/ios-music-player/page.tsx"
"use client"

import * as React from "react"
import { Airplay, Heart, ListMusic, MessageSquareQuote, MoreHorizontal, Pause, Play, SkipBack, SkipForward, Volume1, Volume2 } from "lucide-react"

import { cn } from "@/lib/utils"
import { IOSDevice } from "@/components/ios-kit"

const tracks = [
  { title: "Midnight City", artist: "Neon Harbor", album: "Afterglow", length: 244, art: "from-fuchsia-500 via-purple-600 to-indigo-800" },
  { title: "Slow Tides", artist: "Marlow", album: "Low Season", length: 198, art: "from-cyan-400 via-sky-600 to-blue-900" },
  { title: "Golden Hour", artist: "Pia Okafor", album: "Warm Light", length: 221, art: "from-amber-300 via-orange-500 to-rose-700" },
]

const clock = (seconds: number) => `${Math.floor(seconds / 60)}:${String(Math.floor(seconds % 60)).padStart(2, "0")}`

export default function IOSMusicPlayer() {
  const [{ index, elapsed }, setPlayer] = React.useState({ index: 0, elapsed: 72 })
  const [playing, setPlaying] = React.useState(true)
  const [liked, setLiked] = React.useState(false)
  const [volume, setVolume] = React.useState(65)
  const track = tracks[index]

  React.useEffect(() => {
    if (!playing) return
    const id = setInterval(
      () =>
        setPlayer((p) =>
          p.elapsed + 1 >= tracks[p.index].length
            ? { index: (p.index + 1) % tracks.length, elapsed: 0 }
            : { ...p, elapsed: p.elapsed + 1 }
        ),
      1000
    )
    return () => clearInterval(id)
  }, [playing])

  const go = (next: number) => setPlayer({ index: (next + tracks.length) % tracks.length, elapsed: 0 })
  const setElapsed = (value: number) => setPlayer((p) => ({ ...p, elapsed: value }))
  const position = elapsed

  return (
    <IOSDevice light className="bg-black">
      <div
        aria-hidden
        className={cn("absolute inset-0 bg-gradient-to-b opacity-80 blur-3xl transition-all duration-700", track.art)}
      />
      <div className="absolute inset-0 bg-black/30" aria-hidden />

      <div className="relative flex min-h-0 flex-1 flex-col px-7 pt-[62px] pb-[50px] text-white">
        <div aria-hidden className="mx-auto mb-3 h-[5px] w-9 rounded-full bg-white/40" />

        <div className="flex flex-1 items-center justify-center">
          <div
            className={cn(
              "aspect-square w-full rounded-[14px] bg-gradient-to-br shadow-[0_24px_50px_-10px_rgb(0_0_0/0.55)] transition-all duration-500 ease-[cubic-bezier(0.34,1.4,0.64,1)]",
              track.art,
              playing ? "scale-100" : "scale-[0.84]"
            )}
          >
            <div className="relative size-full overflow-hidden rounded-[14px]">
              <div className="absolute -top-8 -left-8 size-48 rounded-full bg-white/20 blur-2xl" />
              <div className="absolute right-6 bottom-6 size-24 rounded-full border-[14px] border-white/25" />
              <div className="absolute right-[52px] bottom-[52px] size-6 rounded-full bg-white/50" />
            </div>
          </div>
        </div>

        <div className="mt-5 flex items-center justify-between gap-3">
          <div className="min-w-0">
            <p className="truncate text-[22px] leading-7 font-semibold">{track.title}</p>
            <p className="truncate text-[20px] leading-6 text-white/65">{track.artist}</p>
          </div>
          <div className="flex shrink-0 items-center gap-2">
            <button
              type="button"
              aria-label={liked ? "Unlike" : "Like"}
              aria-pressed={liked}
              onClick={() => setLiked((value) => !value)}
              className="flex size-8 items-center justify-center rounded-full bg-white/20 active:scale-90"
            >
              <Heart className={cn("size-4 transition-colors", liked && "fill-white")} />
            </button>
            <button type="button" aria-label="More" className="flex size-8 items-center justify-center rounded-full bg-white/20">
              <MoreHorizontal className="size-4" />
            </button>
          </div>
        </div>

        <div className="mt-4">
          <input
            type="range"
            min={0}
            max={track.length}
            value={position}
            onChange={(event) => setElapsed(Number(event.target.value))}
            aria-label="Playback position"
            className="h-1 w-full cursor-pointer accent-white"
          />
          <div className="mt-1 flex justify-between text-[12px] text-white/60 tabular-nums">
            <span>{clock(position)}</span>
            <span>-{clock(track.length - position)}</span>
          </div>
        </div>

        <div className="mt-2 flex items-center justify-center gap-12">
          <button type="button" aria-label="Previous track" onClick={() => go(index - 1)} className="active:scale-90 active:opacity-60">
            <SkipBack className="size-9" fill="currentColor" />
          </button>
          <button type="button" aria-label={playing ? "Pause" : "Play"} onClick={() => setPlaying((value) => !value)} className="active:scale-90">
            {playing ? <Pause className="size-14" fill="currentColor" /> : <Play className="size-14" fill="currentColor" />}
          </button>
          <button type="button" aria-label="Next track" onClick={() => go(index + 1)} className="active:scale-90 active:opacity-60">
            <SkipForward className="size-9" fill="currentColor" />
          </button>
        </div>

        <div className="mt-4 flex items-center gap-3 text-white/70">
          <Volume1 className="size-4 shrink-0" />
          <input
            type="range"
            min={0}
            max={100}
            value={volume}
            onChange={(event) => setVolume(Number(event.target.value))}
            aria-label="Volume"
            className="h-1 min-w-0 flex-1 cursor-pointer accent-white"
          />
          <Volume2 className="size-4 shrink-0" />
        </div>

        <div className="mt-4 flex items-center justify-around text-white/70">
          <button type="button" aria-label="Lyrics"><MessageSquareQuote className="size-6" /></button>
          <button type="button" aria-label="AirPlay"><Airplay className="size-6" /></button>
          <button type="button" aria-label="Up next"><ListMusic className="size-6" /></button>
        </div>
      </div>
    </IOSDevice>
  )
}
```
