"use client"; import { ListMusic, Pause, Play, Repeat, Repeat1, Shuffle, SkipBack, SkipForward, Volume1, Volume2, VolumeX, X, } from "lucide-react"; import { AnimatePresence, motion, useMotionValue, useReducedMotion, useTransform, } from "motion/react"; import { type ReactNode, useCallback, useEffect, useId, useRef, useState } from "react"; import { Toggle } from "@/components/motion/toggle-group"; import { EASE_IN_OUT, EASE_OUT, SPRING_LAYOUT, SPRING_PANEL, SPRING_SWAP } from "@/lib/ease"; import { useSlider } from "@/lib/hooks/use-slider"; import { cn } from "@/lib/utils"; export interface MusicTrack { id: string; title: string; artist: string; album?: string; /** Length in seconds. Drives the simulated clock when there is no `src`. */ duration: number; /** Cover image URL. Without one a gradient from the theme's chart tokens is drawn. */ cover?: string; /** Optional audio file. Without one playback is simulated. */ src?: string; } export type MusicRepeat = "off" | "all" | "one"; export interface MusicPlayerProps { tracks: MusicTrack[]; /** Controlled track index. */ index?: number; defaultIndex?: number; onIndexChange?: (index: number) => void; /** Controlled playing state. */ playing?: boolean; defaultPlaying?: boolean; onPlayingChange?: (playing: boolean) => void; /** Controlled volume, 0–1. */ volume?: number; defaultVolume?: number; onVolumeChange?: (volume: number) => void; defaultShuffle?: boolean; defaultRepeat?: MusicRepeat; /** Open the queue drawer initially. */ defaultQueueOpen?: boolean; className?: string; } function useControllable( value: T | undefined, defaultValue: T, onChange: ((next: T) => void) | undefined, ) { const [internal, setInternal] = useState(defaultValue); const controlled = value !== undefined; const current = controlled ? value : internal; const set = useCallback( (next: T) => { if (!controlled) setInternal(next); onChange?.(next); }, [controlled, onChange], ); return [current, set] as const; } const formatTime = (seconds: number) => { const s = Math.max(0, Math.floor(seconds)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; }; /** Deterministic shuffle (mulberry32) that keeps `first` at the front. */ function shuffledOrder(length: number, first: number, seed: number) { let t = seed >>> 0; const rand = () => { t += 0x6d2b79f5; let r = Math.imul(t ^ (t >>> 15), 1 | t); r ^= r + Math.imul(r ^ (r >>> 7), 61 | r); return ((r ^ (r >>> 14)) >>> 0) / 4294967296; }; const rest = Array.from({ length }, (_, i) => i).filter((i) => i !== first); for (let i = rest.length - 1; i > 0; i--) { const j = Math.floor(rand() * (i + 1)); [rest[i], rest[j]] = [rest[j], rest[i]]; } return [first, ...rest]; } // Simulated network: the buffer starts this many seconds ahead and fills at // this multiple of real time, so the buffered bar visibly leads the playhead. const SIM_PREBUFFER = 14; const SIM_BUFFER_RATE = 3; /** Two chart tokens per track, picked deterministically from its index. */ function coverGradient(i: number) { const a = (i % 5) + 1; const b = ((i + 2) % 5) + 1; return `radial-gradient(circle at 25% 20%, color-mix(in oklab, var(--chart-${b}) 70%, transparent), transparent 60%), linear-gradient(135deg, var(--chart-${a}), var(--chart-${b}))`; } /** * Now-playing card: cover art that breathes with playback, title/artist that * cross-fade in the skip direction, a scrub bar with buffered and played * layers, a play/pause icon swap, shuffle and repeat toggles, volume, and a * queue drawer that slides up over the card. Playback is simulated unless a * track has a `src`, in which case a hidden `