veza/apps/web/src/components/player/audio-player/AudioPlayerProgress.tsx
senke 6093f8c34c feat(ui): sidebar premium polish + player bar enhancements
Sidebar:
- Discord-style active indicator pill (left edge, primary color)
- Hover micro-animations: icon scale-110, bg transition
- Section dividers between nav groups
- Notification badge pill (primary/15 bg, font-semibold)
- Footer items (Settings, Sign Out) consistent with main nav

Player bar:
- Progress bar: time preview tooltip on hover, scale-y on drag
- Volume: Spotify-style hover-reveal slider, 3-level icon states
- Now playing: ambient glow behind album art on hover
- Track info: clickable artist name with hover underline
- Keyboard shortcuts: N/P (next/prev), Arrow Up/Down (volume), M (mute)
- Shortcut hints in control tooltips (<kbd> badges)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 00:11:11 +01:00

125 lines
3.8 KiB
TypeScript

import { useRef, useState, useCallback } from 'react';
import { Slider } from '@/components/ui/slider';
import { cn } from '@/lib/utils';
import { formatTime } from './types';
interface AudioPlayerProgressProps {
currentTime: number;
duration: number;
onSeek: (value: number[]) => void;
/** "default" shows time labels on sides; "minimal" renders a thin full-width bar (for top-of-player placement). */
variant?: 'default' | 'minimal';
}
export function AudioPlayerProgress({
currentTime,
duration,
onSeek,
variant = 'default',
}: AudioPlayerProgressProps) {
const [hoverTime, setHoverTime] = useState<number | null>(null);
const [tooltipX, setTooltipX] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const barRef = useRef<HTMLDivElement>(null);
const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!barRef.current || !duration) return;
const rect = barRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const pct = Math.max(0, Math.min(1, x / rect.width));
setHoverTime(pct * duration);
setTooltipX(pct * 100);
},
[duration],
);
const handleMouseLeave = useCallback(() => {
if (!isDragging) setHoverTime(null);
}, [isDragging]);
const handlePointerDown = useCallback(() => setIsDragging(true), []);
const handlePointerUp = useCallback(() => {
setIsDragging(false);
setHoverTime(null);
}, []);
if (variant === 'minimal') {
return (
<div
ref={barRef}
className={cn(
'w-full group relative',
isDragging && 'cursor-grabbing',
)}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
>
<Slider
value={[currentTime]}
max={duration || 1}
step={0.1}
onValueChange={onSeek}
className={cn('w-full transition-all', isDragging && 'scale-y-150')}
aria-label="Track progress"
/>
{/* Time preview tooltip */}
{hoverTime !== null && duration > 0 && (
<div
className="absolute bottom-full mb-2 px-2 py-1 bg-card text-foreground text-xs rounded shadow-lg pointer-events-none whitespace-nowrap z-50"
style={{
left: `${tooltipX}%`,
transform: 'translateX(-50%)',
}}
>
{formatTime(hoverTime)}
</div>
)}
</div>
);
}
return (
<div className="flex items-center gap-2 flex-1">
<span className="text-xs text-muted-foreground w-12 text-right">
{formatTime(currentTime)}
</span>
<div
ref={barRef}
className={cn(
'flex-1 group relative',
isDragging && 'cursor-grabbing',
)}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
>
<Slider
value={[currentTime]}
max={duration || 1}
step={0.1}
onValueChange={onSeek}
className={cn('w-full transition-all', isDragging && 'scale-y-150')}
aria-label="Track progress"
/>
{hoverTime !== null && duration > 0 && (
<div
className="absolute bottom-full mb-2 px-2 py-1 bg-card text-foreground text-xs rounded shadow-lg pointer-events-none whitespace-nowrap z-50"
style={{
left: `${tooltipX}%`,
transform: 'translateX(-50%)',
}}
>
{formatTime(hoverTime)}
</div>
)}
</div>
<span className="text-xs text-muted-foreground w-12">
{formatTime(duration)}
</span>
</div>
);
}