@@ -11,7 +11,15 @@ import { useProjectId, useVideoProjectStore } from "@/data/store";
1111import { cn , resolveDuration } from "@/lib/utils" ;
1212import { useMutation , useQuery , useQueryClient } from "@tanstack/react-query" ;
1313import { useTranslations } from "next-intl" ;
14- import { type DragEventHandler , useEffect , useMemo , useState } from "react" ;
14+ import {
15+ type DragEventHandler ,
16+ type KeyboardEventHandler ,
17+ type MouseEventHandler ,
18+ useEffect ,
19+ useMemo ,
20+ useRef ,
21+ useState ,
22+ } from "react" ;
1523import { VideoControls } from "./video-controls" ;
1624import { TimelineRuler } from "./video/timeline" ;
1725import { VideoTrackRow } from "./video/track" ;
@@ -20,13 +28,26 @@ export default function BottomBar() {
2028 const t = useTranslations ( "app.bottomBar" ) ;
2129 const queryClient = useQueryClient ( ) ;
2230 const projectId = useProjectId ( ) ;
31+ const player = useVideoProjectStore ( ( s ) => s . player ) ;
2332 const playerCurrentTimestamp = useVideoProjectStore (
2433 ( s ) => s . playerCurrentTimestamp ,
2534 ) ;
26- const formattedTimestamp =
27- ( playerCurrentTimestamp < 10 ? "0" : "" ) +
28- playerCurrentTimestamp . toFixed ( 2 ) ;
29- const minTrackWidth = `${ ( ( 2 / 30 ) * 100 ) . toFixed ( 2 ) } %` ;
35+ const setPlayerCurrentTimestamp = useVideoProjectStore (
36+ ( s ) => s . setPlayerCurrentTimestamp ,
37+ ) ;
38+ const timelineWrapperRef = useRef < HTMLDivElement > ( null ) ;
39+ const timelineDurationSeconds = 30 ;
40+ const FPS = 30 ;
41+ const minTrackWidth = `${ ( ( 2 / timelineDurationSeconds ) * 100 ) . toFixed ( 2 ) } %` ;
42+ const currentMinutes = Math . floor ( playerCurrentTimestamp / 60 ) ;
43+ const formattedCurrentMinutes = currentMinutes . toString ( ) . padStart ( 2 , "0" ) ;
44+ const currentSeconds = playerCurrentTimestamp % 60 ;
45+ const formattedCurrentSeconds = currentSeconds . toFixed ( 2 ) . padStart ( 5 , "0" ) ;
46+ const totalMinutes = Math . floor ( timelineDurationSeconds / 60 ) ;
47+ const formattedTotalMinutes = totalMinutes . toString ( ) . padStart ( 2 , "0" ) ;
48+ const formattedTotalSeconds = ( timelineDurationSeconds % 60 )
49+ . toFixed ( 2 )
50+ . padStart ( 5 , "0" ) ;
3051 const [ dragOverTracks , setDragOverTracks ] = useState ( false ) ;
3152
3253 const limitAllKeyframesToThirtySeconds = useMutation ( {
@@ -181,16 +202,81 @@ export default function BottomBar() {
181202 return true ;
182203 } ;
183204
205+ const seekToTimestamp = ( nextTimestamp : number ) => {
206+ const clampedTimestamp = Math . max (
207+ 0 ,
208+ Math . min ( nextTimestamp , timelineDurationSeconds ) ,
209+ ) ;
210+
211+ if ( player ) {
212+ const frame = Math . round ( clampedTimestamp * FPS ) ;
213+ player . seekTo ( frame ) ;
214+ }
215+
216+ setPlayerCurrentTimestamp ( clampedTimestamp ) ;
217+ } ;
218+
219+ const handleTimelineClick : MouseEventHandler < HTMLDivElement > = ( event ) => {
220+ const timelineElement = timelineWrapperRef . current ;
221+ if ( ! timelineElement ) return ;
222+
223+ const rect = timelineElement . getBoundingClientRect ( ) ;
224+ if ( rect . width === 0 ) return ;
225+
226+ const relativeX = event . clientX - rect . left ;
227+ const clampedX = Math . min ( Math . max ( relativeX , 0 ) , rect . width ) ;
228+ const ratio = clampedX / rect . width ;
229+ const nextTimestamp = timelineDurationSeconds * ratio ;
230+
231+ seekToTimestamp ( nextTimestamp ) ;
232+ } ;
233+
234+ const handleTimelineKeyDown : KeyboardEventHandler < HTMLDivElement > = (
235+ event ,
236+ ) => {
237+ switch ( event . key ) {
238+ case "ArrowLeft" :
239+ event . preventDefault ( ) ;
240+ seekToTimestamp ( playerCurrentTimestamp - 1 ) ;
241+ break ;
242+ case "ArrowRight" :
243+ event . preventDefault ( ) ;
244+ seekToTimestamp ( playerCurrentTimestamp + 1 ) ;
245+ break ;
246+ case "Home" :
247+ event . preventDefault ( ) ;
248+ seekToTimestamp ( 0 ) ;
249+ break ;
250+ case "End" :
251+ event . preventDefault ( ) ;
252+ seekToTimestamp ( timelineDurationSeconds ) ;
253+ break ;
254+ default :
255+ break ;
256+ }
257+ } ;
258+
259+ const timelineProgressPercent = (
260+ ( Math . max ( 0 , Math . min ( playerCurrentTimestamp , timelineDurationSeconds ) ) /
261+ timelineDurationSeconds ) *
262+ 100
263+ ) . toFixed ( 2 ) ;
264+
184265 return (
185266 < div className = "border-t pb-2 border-border flex flex-col bg-background-light " >
186267 < div className = "border-b border-border bg-background-dark px-2 flex flex-row gap-8 py-2 justify-between items-center flex-1" >
187268 < div className = "h-full flex flex-col justify-center px-4 bg-muted/50 rounded-md font-mono cursor-default select-none shadow-inner" >
188269 < div className = "flex flex-row items-baseline font-thin tabular-nums" >
189- < span className = "text-muted-foreground" > 00:</ span >
190- < span > { formattedTimestamp } </ span >
270+ < span className = "text-muted-foreground" >
271+ { formattedCurrentMinutes } :
272+ </ span >
273+ < span > { formattedCurrentSeconds } </ span >
191274 < span className = "text-muted-foreground/50 mx-2" > /</ span >
192275 < span className = "text-sm opacity-50" >
193- < span className = "text-muted-foreground" > 00:</ span > 30.00
276+ < span className = "text-muted-foreground" >
277+ { formattedTotalMinutes } :
278+ </ span >
279+ { formattedTotalSeconds }
194280 </ span >
195281 </ div >
196282 </ div >
@@ -207,14 +293,26 @@ export default function BottomBar() {
207293 onDragLeave = { ( ) => setDragOverTracks ( false ) }
208294 onDrop = { handleOnDrop }
209295 >
210- < div className = "flex flex-col justify-start w-full h-full relative" >
296+ < div
297+ ref = { timelineWrapperRef }
298+ className = "flex flex-col justify-start w-full h-full relative"
299+ role = "slider"
300+ aria-label = "Timeline"
301+ aria-valuemin = { 0 }
302+ aria-valuemax = { timelineDurationSeconds }
303+ aria-valuenow = { playerCurrentTimestamp }
304+ aria-valuetext = { `${ formattedCurrentMinutes } :${ formattedCurrentSeconds } ` }
305+ tabIndex = { 0 }
306+ onClick = { handleTimelineClick }
307+ onKeyDown = { handleTimelineKeyDown }
308+ >
211309 < div
212310 className = "absolute z-[32] top-6 bottom-0 w-[2px] bg-white/30 ms-4"
213311 style = { {
214- left : `${ ( ( playerCurrentTimestamp / 30 ) * 100 ) . toFixed ( 2 ) } %` ,
312+ left : `${ timelineProgressPercent } %` ,
215313 } }
216314 />
217- < TimelineRuler className = "z-30 pointer-events-none " />
315+ < TimelineRuler className = "z-30" />
218316 < div className = "flex timeline-container flex-col h-full mx-4 mt-10 gap-2 z-[31] pb-2" >
219317 { Object . entries ( trackObj ) . map ( ( [ trackType , track ] ) =>
220318 track ? (
0 commit comments