veza/apps/web/src/components/player/LyricsPanel.tsx
senke 73e8372b0e refactor: Phase 7 — Clean up legacy components and remove dead tokens
- Bulk replace text-white → text-foreground across 116 component files
  (preserving text-white/ opacity variants)
- Remove hover-glow-cyan, shadow-card-glow-cyan, shadow-button-primary-glow
  classes from all components
- Replace --duration-normal/--duration-immersive/--duration-slow with
  --sumi-duration-normal/--sumi-duration-slow across 130+ files
- Replace --ease-out/--ease-in-out with --sumi-ease-out/--sumi-ease-in-out
- Replace focus:ring-blue-500 → focus:ring-primary (4 files)
- Remove hover:scale-105/110 and hover:-translate-y-1/0.5 transforms
  (SUMI anti-pattern: no scale on hover)
- Clean up stale kodo- references in comments

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 02:09:29 +01:00

80 lines
2.8 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
import { useAudio } from '../../context/AudioContext';
import { Mic2, AlignLeft } from 'lucide-react';
export const LyricsPanel: React.FC = () => {
const { currentTrack, currentTime, seek, duration } = useAudio();
const scrollRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true);
// Auto-scroll logic
useEffect(() => {
if (autoScroll && scrollRef.current && currentTrack?.lyrics) {
const activeIndex = currentTrack.lyrics.findIndex(
(line: { time: number; text: string }, i: number) => {
return (
currentTime >= line.time &&
(i === currentTrack.lyrics!.length - 1 ||
currentTime < currentTrack.lyrics![i + 1].time)
);
},
);
if (activeIndex !== -1) {
const element = scrollRef.current.children[activeIndex] as HTMLElement;
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
}, [currentTime, currentTrack, autoScroll]);
if (!currentTrack?.lyrics) {
return (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground opacity-50">
<Mic2 className="w-16 h-16 mb-4" />
<p>No lyrics available</p>
</div>
);
}
return (
<div
className="h-full flex flex-col relative group"
onMouseEnter={() => setAutoScroll(false)}
onMouseLeave={() => setAutoScroll(true)}
>
<div className="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => setAutoScroll(!autoScroll)}
className={`p-2 rounded-full backdrop-blur-md ${autoScroll ? 'bg-primary/20 text-primary' : 'bg-black/30 text-muted-foreground'}`}
title="Auto-scroll"
>
<AlignLeft className="w-4 h-4" />
</button>
</div>
<div
ref={scrollRef}
className="flex-1 overflow-y-auto custom-scrollbar px-4 space-y-6 text-center mask-image-linear-to-b py-24"
>
{currentTrack.lyrics.map(
(line: { time: number; text: string }, i: number) => {
const isActive =
currentTime >= line.time &&
(i === currentTrack.lyrics!.length - 1 ||
currentTime < currentTrack.lyrics![i + 1].time);
return (
<p
key={i}
className={`text-2xl md:text-3xl font-bold transition-all duration-[var(--sumi-duration-slow)] cursor-pointer hover:text-foreground ${isActive ? 'text-foreground scale-105 origin-center' : 'text-white/20 blur-[1px]'}`}
onClick={() => seek((line.time / duration) * 100)}
>
{line.text}
</p>
);
},
)}
</div>
</div>
);
};