{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "music-player-card", "type": "registry:ui", "title": "Music Player Card", "description": "A rich music player card with album art, action pills, seekable progress, shuffle, repeat, and full transport controls.", "dependencies": [ "lucide-react" ], "files": [ { "path": "registry/ruixenui/music-player-card.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n Heart,\n ThumbsUp,\n ThumbsDown,\n MessageSquare,\n ListPlus,\n Share2,\n Shuffle,\n SkipBack,\n Pause,\n Play,\n SkipForward,\n Repeat,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface Track {\n id: string;\n title: string;\n artist: string;\n cover: string;\n duration: string;\n src: string;\n likes?: string;\n comments?: string;\n}\n\ninterface MusicPlayerCardProps {\n tracks: Track[];\n className?: string;\n}\n\nfunction fmt(s: number) {\n const m = Math.floor(s / 60);\n const sec = Math.floor(s % 60);\n return `${m}:${sec < 10 ? \"0\" : \"\"}${sec}`;\n}\n\nexport default function MusicPlayerCard({\n tracks,\n className,\n}: MusicPlayerCardProps) {\n const audioRef = React.useRef(null);\n const progressRef = React.useRef(null);\n const playingRef = React.useRef(false);\n\n const [trackIndex, setTrackIndex] = React.useState(0);\n const [isPlaying, setIsPlaying] = React.useState(false);\n const [progress, setProgress] = React.useState(0);\n const [currentTime, setCurrentTime] = React.useState(\"0:00\");\n const [liked, setLiked] = React.useState(false);\n const [shuffleOn, setShuffleOn] = React.useState(false);\n const [repeatOn, setRepeatOn] = React.useState(false);\n\n const track = tracks[trackIndex];\n\n React.useEffect(() => {\n playingRef.current = isPlaying;\n }, [isPlaying]);\n\n React.useEffect(() => {\n if (!track) return;\n\n setProgress(0);\n setCurrentTime(\"0:00\");\n\n const audio = new Audio(track.src);\n audioRef.current = audio;\n\n const onTime = () => {\n setProgress((audio.currentTime / (audio.duration || 1)) * 100);\n setCurrentTime(fmt(audio.currentTime));\n };\n\n const onEnd = () => {\n if (repeatOn) {\n audio.currentTime = 0;\n audio.play().catch(() => {});\n } else if (trackIndex < tracks.length - 1) {\n setTrackIndex((i) => i + 1);\n } else {\n setIsPlaying(false);\n setProgress(0);\n setCurrentTime(\"0:00\");\n }\n };\n\n audio.addEventListener(\"timeupdate\", onTime);\n audio.addEventListener(\"ended\", onEnd);\n\n if (playingRef.current) {\n audio.play().catch(() => setIsPlaying(false));\n }\n\n return () => {\n audio.removeEventListener(\"timeupdate\", onTime);\n audio.removeEventListener(\"ended\", onEnd);\n audio.pause();\n audio.src = \"\";\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [trackIndex]);\n\n const togglePlay = () => {\n const audio = audioRef.current;\n if (!audio) return;\n if (isPlaying) {\n audio.pause();\n setIsPlaying(false);\n } else {\n audio.play().catch(() => {});\n setIsPlaying(true);\n }\n };\n\n const prevTrack = () => {\n if (audioRef.current && audioRef.current.currentTime > 3) {\n audioRef.current.currentTime = 0;\n } else {\n setTrackIndex((i) => (i > 0 ? i - 1 : tracks.length - 1));\n }\n };\n\n const nextTrack = () => {\n if (shuffleOn && tracks.length > 1) {\n let next: number;\n do {\n next = Math.floor(Math.random() * tracks.length);\n } while (next === trackIndex);\n setTrackIndex(next);\n } else {\n setTrackIndex((i) => (i < tracks.length - 1 ? i + 1 : 0));\n }\n };\n\n const seek = (e: React.PointerEvent | PointerEvent) => {\n const bar = progressRef.current;\n const audio = audioRef.current;\n if (!bar || !audio || !audio.duration) return;\n const rect = bar.getBoundingClientRect();\n const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));\n audio.currentTime = pct * audio.duration;\n };\n\n const onPointerDown = (e: React.PointerEvent) => {\n seek(e);\n const onMove = (ev: PointerEvent) => seek(ev);\n const onUp = () => {\n window.removeEventListener(\"pointermove\", onMove);\n window.removeEventListener(\"pointerup\", onUp);\n };\n window.addEventListener(\"pointermove\", onMove);\n window.addEventListener(\"pointerup\", onUp);\n };\n\n if (!track) return null;\n\n return (\n \n {/* album art + heart */}\n
\n \n setLiked((l) => !l)}\n className=\"absolute right-2.5 top-2.5 transition-transform duration-200 active:scale-90\"\n aria-label={liked ? \"Unlike\" : \"Like\"}\n >\n \n \n
\n\n {/* title + artist */}\n
\n

\n {track.title}\n

\n

\n {track.artist}\n

\n
\n\n {/* action pills */}\n
\n {/* like / dislike */}\n
\n \n {track.likes && (\n \n {track.likes}\n \n )}\n
\n \n
\n\n {/* comments */}\n {track.comments && (\n
\n \n \n {track.comments}\n \n
\n )}\n\n {/* save */}\n \n\n {/* share */}\n \n
\n\n {/* progress bar */}\n
\n \n \n \n
\n
\n \n {currentTime}\n \n \n {track.duration}\n \n
\n
\n\n {/* transport controls */}\n
\n setShuffleOn((s) => !s)}\n className={cn(\n \"transition-colors duration-200\",\n shuffleOn\n ? \"text-foreground\"\n : \"text-foreground/30 hover:text-foreground/50\",\n )}\n aria-label=\"Shuffle\"\n >\n \n \n\n \n \n \n\n \n {isPlaying ? (\n \n ) : (\n \n )}\n \n\n \n \n \n\n setRepeatOn((r) => !r)}\n className={cn(\n \"transition-colors duration-200\",\n repeatOn\n ? \"text-foreground\"\n : \"text-foreground/30 hover:text-foreground/50\",\n )}\n aria-label=\"Repeat\"\n >\n \n \n
\n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/music-player-card.tsx" } ] }