2025-07-15 04:23:45 +06:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { SnapPoint } from "@/hooks/use-timeline-snapping";
|
|
|
|
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
2025-07-15 14:16:54 +02:00
|
|
|
import type { TimelineTrack } from "@/types/timeline";
|
2025-07-26 22:25:57 +02:00
|
|
|
import { useState, useEffect } from "react";
|
2025-07-15 04:23:45 +06:00
|
|
|
|
|
|
|
|
interface SnapIndicatorProps {
|
|
|
|
|
snapPoint: SnapPoint | null;
|
|
|
|
|
zoomLevel: number;
|
|
|
|
|
isVisible: boolean;
|
2025-07-15 14:16:54 +02:00
|
|
|
tracks: TimelineTrack[];
|
|
|
|
|
timelineRef: React.RefObject<HTMLDivElement>;
|
|
|
|
|
trackLabelsRef?: React.RefObject<HTMLDivElement>;
|
2025-07-26 22:25:57 +02:00
|
|
|
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
2025-07-15 04:23:45 +06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function SnapIndicator({
|
|
|
|
|
snapPoint,
|
|
|
|
|
zoomLevel,
|
|
|
|
|
isVisible,
|
2025-07-15 14:16:54 +02:00
|
|
|
tracks,
|
|
|
|
|
timelineRef,
|
|
|
|
|
trackLabelsRef,
|
2025-07-26 22:25:57 +02:00
|
|
|
tracksScrollRef,
|
2025-07-15 04:23:45 +06:00
|
|
|
}: SnapIndicatorProps) {
|
2025-07-26 22:25:57 +02:00
|
|
|
const [scrollLeft, setScrollLeft] = useState(0);
|
|
|
|
|
|
|
|
|
|
// Track scroll position to lock snap indicator to frame
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const tracksViewport = tracksScrollRef.current?.querySelector(
|
|
|
|
|
"[data-radix-scroll-area-viewport]"
|
|
|
|
|
) as HTMLElement;
|
|
|
|
|
|
|
|
|
|
if (!tracksViewport) return;
|
|
|
|
|
|
|
|
|
|
const handleScroll = () => {
|
|
|
|
|
setScrollLeft(tracksViewport.scrollLeft);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Set initial scroll position
|
|
|
|
|
setScrollLeft(tracksViewport.scrollLeft);
|
|
|
|
|
|
|
|
|
|
tracksViewport.addEventListener("scroll", handleScroll);
|
|
|
|
|
return () => tracksViewport.removeEventListener("scroll", handleScroll);
|
|
|
|
|
}, [tracksScrollRef]);
|
|
|
|
|
|
2025-07-15 04:23:45 +06:00
|
|
|
if (!isVisible || !snapPoint) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-15 14:16:54 +02:00
|
|
|
const timelineContainerHeight = timelineRef.current?.offsetHeight || 400;
|
|
|
|
|
const totalHeight = timelineContainerHeight - 8; // 8px padding from edges
|
|
|
|
|
|
|
|
|
|
// Get dynamic track labels width, fallback to 0 if no tracks or no ref
|
|
|
|
|
const trackLabelsWidth =
|
|
|
|
|
tracks.length > 0 && trackLabelsRef?.current
|
|
|
|
|
? trackLabelsRef.current.offsetWidth
|
|
|
|
|
: 0;
|
|
|
|
|
|
2025-07-26 22:25:57 +02:00
|
|
|
// Calculate position locked to timeline content (accounting for scroll)
|
|
|
|
|
const timelinePosition =
|
2025-07-15 04:23:45 +06:00
|
|
|
snapPoint.time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
2025-07-26 22:25:57 +02:00
|
|
|
const leftPosition = trackLabelsWidth + timelinePosition - scrollLeft;
|
2025-07-15 04:23:45 +06:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
2025-07-30 13:15:09 -04:00
|
|
|
className="absolute pointer-events-none z-90"
|
2025-07-15 04:23:45 +06:00
|
|
|
style={{
|
|
|
|
|
left: `${leftPosition}px`,
|
2025-07-15 14:16:54 +02:00
|
|
|
top: 0,
|
|
|
|
|
height: `${totalHeight}px`,
|
|
|
|
|
width: "2px",
|
2025-07-15 04:23:45 +06:00
|
|
|
}}
|
|
|
|
|
>
|
2025-07-24 14:41:34 -07:00
|
|
|
<div className={"w-0.5 h-full bg-primary/40 opacity-80"} />
|
2025-07-15 04:23:45 +06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|