Files
mediaproc/ui/timeline/src/Timeline.tsx
2026-02-06 09:41:50 -03:00

122 lines
3.7 KiB
TypeScript

import { useRef, useCallback, useState, useEffect } from "react";
interface TimelineProps {
duration: number;
currentTime: number;
trimStart: number;
trimEnd: number;
onTrimChange: (start: number, end: number) => void;
onSeek: (time: number) => void;
}
function formatTime(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
const ms = Math.floor((seconds % 1) * 10);
return `${m}:${s.toString().padStart(2, "0")}.${ms}`;
}
export default function Timeline({
duration,
currentTime,
trimStart,
trimEnd,
onTrimChange,
onSeek,
}: TimelineProps) {
const trackRef = useRef<HTMLDivElement>(null);
const [dragging, setDragging] = useState<"in" | "out" | null>(null);
const timeToPercent = (t: number) => (duration > 0 ? (t / duration) * 100 : 0);
const positionToTime = useCallback(
(clientX: number) => {
const track = trackRef.current;
if (!track || duration <= 0) return 0;
const rect = track.getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
return ratio * duration;
},
[duration],
);
const handleTrackClick = (e: React.MouseEvent) => {
if (dragging) return;
onSeek(positionToTime(e.clientX));
};
const handleMouseDown = (handle: "in" | "out") => (e: React.MouseEvent) => {
e.stopPropagation();
setDragging(handle);
};
useEffect(() => {
if (!dragging) return;
const minGap = 0.1;
const handleMove = (e: MouseEvent) => {
const time = positionToTime(e.clientX);
if (dragging === "in") {
onTrimChange(Math.min(time, trimEnd - minGap), trimEnd);
} else {
onTrimChange(trimStart, Math.max(time, trimStart + minGap));
}
};
const handleUp = () => setDragging(null);
document.addEventListener("mousemove", handleMove);
document.addEventListener("mouseup", handleUp);
return () => {
document.removeEventListener("mousemove", handleMove);
document.removeEventListener("mouseup", handleUp);
};
}, [dragging, trimStart, trimEnd, positionToTime, onTrimChange]);
const inPct = timeToPercent(trimStart);
const outPct = timeToPercent(trimEnd);
const playheadPct = timeToPercent(currentTime);
const selectionDuration = trimEnd - trimStart;
return (
<div className="timeline">
<div className="timeline-times">
<span>In: {formatTime(trimStart)}</span>
<span>Selection: {formatTime(selectionDuration)}</span>
<span>Out: {formatTime(trimEnd)}</span>
</div>
<div className="timeline-track" ref={trackRef} onClick={handleTrackClick}>
{/* Dimmed regions */}
<div className="timeline-dim" style={{ left: 0, width: `${inPct}%` }} />
<div className="timeline-dim" style={{ left: `${outPct}%`, width: `${100 - outPct}%` }} />
{/* Selection highlight */}
<div
className="timeline-selection"
style={{ left: `${inPct}%`, width: `${outPct - inPct}%` }}
/>
{/* Playhead */}
<div className="timeline-playhead" style={{ left: `${playheadPct}%` }} />
{/* Handles */}
<div
className={`timeline-handle timeline-handle-in ${dragging === "in" ? "dragging" : ""}`}
style={{ left: `${inPct}%` }}
onMouseDown={handleMouseDown("in")}
/>
<div
className={`timeline-handle timeline-handle-out ${dragging === "out" ? "dragging" : ""}`}
style={{ left: `${outPct}%` }}
onMouseDown={handleMouseDown("out")}
/>
</div>
<div className="timeline-duration">
<span>0:00</span>
<span>{formatTime(duration)}</span>
</div>
</div>
);
}