import useDraggableElement from "@/hooks/use-draggable-element"; import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { cn } from "@/lib/utils"; import { DraggableElement } from "@/types/draggable-element"; import { TimelineZoomDirection } from "@/types/review"; import { ReactNode, RefObject, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { isIOS, isMobile } from "react-device-detect"; export type ReviewTimelineProps = { timelineRef: RefObject; contentRef: RefObject; segmentDuration: number; timelineDuration: number; timelineStartAligned: number; showHandlebar: boolean; showExportHandles: boolean; handlebarTime?: number; setHandlebarTime?: React.Dispatch>; onHandlebarDraggingChange?: (isDragging: boolean) => void; onlyInitialHandlebarScroll?: boolean; exportStartTime?: number; exportEndTime?: number; setExportStartTime?: React.Dispatch>; setExportEndTime?: React.Dispatch>; timelineCollapsed?: boolean; dense: boolean; segments: number[]; scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void; isZooming: boolean; zoomDirection: TimelineZoomDirection; children: ReactNode; }; export function ReviewTimeline({ timelineRef, contentRef, segmentDuration, timelineDuration, timelineStartAligned, showHandlebar = false, showExportHandles = false, handlebarTime, setHandlebarTime, onHandlebarDraggingChange, onlyInitialHandlebarScroll = false, exportStartTime, setExportStartTime, exportEndTime, setExportEndTime, timelineCollapsed = false, dense, segments, scrollToSegment, isZooming, zoomDirection, children, }: ReviewTimelineProps) { const [isDraggingHandlebar, setIsDraggingHandlebar] = useState(false); const [isDraggingExportStart, setIsDraggingExportStart] = useState(false); const [isDraggingExportEnd, setIsDraggingExportEnd] = useState(false); const [exportStartPosition, setExportStartPosition] = useState(0); const [exportEndPosition, setExportEndPosition] = useState(0); const segmentsRef = useRef(null); const handlebarRef = useRef(null); const handlebarTimeRef = useRef(null); const exportStartRef = useRef(null); const exportStartTimeRef = useRef(null); const exportEndRef = useRef(null); const exportEndTimeRef = useRef(null); const isDragging = useMemo( () => isDraggingHandlebar || isDraggingExportStart || isDraggingExportEnd, [isDraggingHandlebar, isDraggingExportStart, isDraggingExportEnd], ); const exportSectionRef = useRef(null); const [draggableElementType, setDraggableElementType] = useState(); const { alignStartDateToTimeline, alignEndDateToTimeline, segmentHeight } = useTimelineUtils({ segmentDuration, timelineDuration, timelineRef, }); const paddedExportStartTime = useMemo(() => { if (exportStartTime) { return alignStartDateToTimeline(exportStartTime) + segmentDuration; } }, [exportStartTime, segmentDuration, alignStartDateToTimeline]); const paddedExportEndTime = useMemo(() => { if (exportEndTime) { return alignEndDateToTimeline(exportEndTime); } }, [exportEndTime, alignEndDateToTimeline]); const { handleMouseDown: handlebarMouseDown, handleMouseUp: handlebarMouseUp, handleMouseMove: handlebarMouseMove, } = useDraggableElement({ contentRef, timelineRef, segmentsRef, draggableElementRef: handlebarRef, segmentDuration, showDraggableElement: showHandlebar, draggableElementTime: handlebarTime, setDraggableElementTime: setHandlebarTime, alignSetTimeToSegment: true, initialScrollIntoViewOnly: onlyInitialHandlebarScroll, timelineDuration, timelineCollapsed: timelineCollapsed, timelineStartAligned, isDragging: isDraggingHandlebar, setIsDragging: setIsDraggingHandlebar, draggableElementTimeRef: handlebarTimeRef, dense, segments, scrollToSegment, }); const { handleMouseDown: exportStartMouseDown, handleMouseUp: exportStartMouseUp, handleMouseMove: exportStartMouseMove, } = useDraggableElement({ contentRef, timelineRef, segmentsRef, draggableElementRef: exportStartRef, segmentDuration, showDraggableElement: showExportHandles, draggableElementTime: exportStartTime, draggableElementLatestTime: paddedExportEndTime, setDraggableElementTime: setExportStartTime, timelineDuration, timelineStartAligned, isDragging: isDraggingExportStart, setIsDragging: setIsDraggingExportStart, draggableElementTimeRef: exportStartTimeRef, setDraggableElementPosition: setExportStartPosition, dense, segments, scrollToSegment, }); const { handleMouseDown: exportEndMouseDown, handleMouseUp: exportEndMouseUp, handleMouseMove: exportEndMouseMove, } = useDraggableElement({ contentRef, timelineRef, segmentsRef, draggableElementRef: exportEndRef, segmentDuration, showDraggableElement: showExportHandles, draggableElementTime: exportEndTime, draggableElementEarliestTime: paddedExportStartTime, setDraggableElementTime: setExportEndTime, timelineDuration, timelineStartAligned, isDragging: isDraggingExportEnd, setIsDragging: setIsDraggingExportEnd, draggableElementTimeRef: exportEndTimeRef, setDraggableElementPosition: setExportEndPosition, dense, segments, scrollToSegment, }); const handleHandlebar = useCallback( ( e: | React.MouseEvent | React.TouchEvent, ) => { setDraggableElementType("handlebar"); handlebarMouseDown(e); }, [handlebarMouseDown], ); const handleExportStart = useCallback( ( e: | React.MouseEvent | React.TouchEvent, ) => { setDraggableElementType("export_start"); exportStartMouseDown(e); }, [exportStartMouseDown], ); const handleExportEnd = useCallback( ( e: | React.MouseEvent | React.TouchEvent, ) => { setDraggableElementType("export_end"); exportEndMouseDown(e); }, [exportEndMouseDown], ); const handleMouseMove = useCallback( (e: MouseEvent | TouchEvent) => { switch (draggableElementType) { case "export_start": exportStartMouseMove(e); break; case "export_end": exportEndMouseMove(e); break; case "handlebar": handlebarMouseMove(e); break; default: break; } }, [ draggableElementType, exportStartMouseMove, exportEndMouseMove, handlebarMouseMove, ], ); const handleMouseUp = useCallback( (e: MouseEvent | TouchEvent) => { switch (draggableElementType) { case "export_start": exportStartMouseUp(e); break; case "export_end": exportEndMouseUp(e); break; case "handlebar": handlebarMouseUp(e); break; default: break; } }, [ draggableElementType, exportStartMouseUp, exportEndMouseUp, handlebarMouseUp, ], ); const textSizeClasses = useCallback( (draggableElement: DraggableElement) => { if (isDragging && isMobile && draggableElementType === draggableElement) { return "text-lg"; } else if (dense) { return "text-[8px] md:text-[11px]"; } else { return "text-[11px]"; } }, [dense, isDragging, draggableElementType], ); useEffect(() => { if ( exportSectionRef.current && segmentHeight && exportStartPosition && exportEndPosition ) { exportSectionRef.current.style.top = `${exportEndPosition + segmentHeight}px`; exportSectionRef.current.style.height = `${exportStartPosition - exportEndPosition + segmentHeight / 2}px`; } }, [ showExportHandles, segmentHeight, timelineRef, exportStartPosition, exportEndPosition, ]); const documentRef = useRef(document); useEffect(() => { const documentInstance = documentRef.current; if (isDragging) { documentInstance?.addEventListener("mousemove", handleMouseMove); documentInstance?.addEventListener("touchmove", handleMouseMove); documentInstance?.addEventListener("mouseup", handleMouseUp); documentInstance?.addEventListener("touchend", handleMouseUp); } else { documentInstance?.removeEventListener("mousemove", handleMouseMove); documentInstance?.removeEventListener("touchmove", handleMouseMove); documentInstance?.removeEventListener("mouseup", handleMouseUp); documentInstance?.removeEventListener("touchend", handleMouseUp); } return () => { documentInstance?.removeEventListener("mousemove", handleMouseMove); documentInstance?.removeEventListener("touchmove", handleMouseMove); documentInstance?.removeEventListener("mouseup", handleMouseUp); documentInstance?.removeEventListener("touchend", handleMouseUp); }; }, [handleMouseMove, handleMouseUp, isDragging]); useEffect(() => { if (onHandlebarDraggingChange) { onHandlebarDraggingChange(isDraggingHandlebar); } }, [isDraggingHandlebar, onHandlebarDraggingChange]); return (
{children}
{children && ( <> {showHandlebar && (
)} {showExportHandles && ( <>
)} )}
); } export default ReviewTimeline;