mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 23:29:50 +00:00
600 lines
22 KiB
TypeScript
600 lines
22 KiB
TypeScript
import { useRef, useMemo, useCallback, useState, memo } from "react";
|
|
import { useMusicBeatAnalysis } from "../../hooks/useMusicBeatAnalysis";
|
|
import { remapBeatAnalysisToComposition } from "../../utils/beatEditActions";
|
|
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
import { useExpandedTimelineElements } from "../hooks/useExpandedTimelineElements";
|
|
import { defaultTimelineTheme } from "./timelineTheme";
|
|
import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
|
import { useTimelinePlayhead } from "./useTimelinePlayhead";
|
|
import { useTimelineZoom } from "./useTimelineZoom";
|
|
import { useTimelineAssetDrop } from "./timelineDragDrop";
|
|
import { TimelineEmptyState } from "./TimelineEmptyState";
|
|
import { TimelineCanvas } from "./TimelineCanvas";
|
|
import { type KeyframeDiamondContextMenuState } from "./KeyframeDiamondContextMenu";
|
|
import { useTimelineClipDrag } from "./useTimelineClipDrag";
|
|
import { TimelineOverlays, type ClipContextMenuState } from "./TimelineOverlays";
|
|
import { useTimelineEditPinning } from "./useTimelineEditPinning";
|
|
import { useTimelineStackingSync } from "./useTimelineStackingSync";
|
|
import { useTimelineGeometry } from "./useTimelineGeometry";
|
|
import { useAutoExpandKeyframedClips } from "./useAutoExpandKeyframedClips";
|
|
import { GUTTER, LABEL_COL_W, TRACKS_LEFT_PAD } from "./timelineLayout";
|
|
import { useTimelineScrollViewport } from "./useTimelineScrollViewport";
|
|
import { useResolvedTimelineEditCallbacks } from "./useResolvedTimelineEditCallbacks";
|
|
import type { TimelineProps } from "./TimelineTypes";
|
|
import {
|
|
getTrackStyle,
|
|
useTimelineDisplayLayout,
|
|
useTimelineTrackLayout,
|
|
} from "./useTimelineTrackLayout";
|
|
import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
|
|
import { useTrackGapMenu } from "./useTrackGapMenu";
|
|
import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
|
|
import { TimelineRazorGuide, useTimelineRazorInteraction } from "./TimelineRazorInteraction";
|
|
import { useTimelinePerformanceTelemetry } from "./useTimelinePerformanceTelemetry";
|
|
import {
|
|
getEffectiveTimelineDuration,
|
|
getTimelinePreviewElement,
|
|
hasKeyframedTimelineClips,
|
|
} from "./timelineViewModel";
|
|
import { useTimelineSelectionLifecycle } from "./useTimelineSelectionLifecycle";
|
|
import { useTimelineShiftModifier } from "./useTimelineShiftModifier";
|
|
import { useTimelineTicks } from "./useTimelineTicks";
|
|
import { getTimelineElementIndexes } from "../lib/timelineElementIndexes";
|
|
import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
|
|
import { useTimelineRowVirtualization } from "./useTimelineRowVirtualization";
|
|
import { useTimelineClipRenderWindow } from "./useTimelineClipRenderWindow";
|
|
import { useTimelineActiveClips } from "./useTimelineActiveClips";
|
|
import { useTimelineLaneMoveRefresh } from "./useTimelineLaneMoveRefresh";
|
|
|
|
export {
|
|
shouldAutoScrollTimeline,
|
|
getTimelineScrollLeftForZoomTransition,
|
|
getTimelineScrollLeftForZoomAnchor,
|
|
getTimelinePlaybackFollowScrollLeft,
|
|
getTimelinePlayheadLeft,
|
|
getTimelineCanvasHeight,
|
|
shouldShowTimelineShortcutHint,
|
|
resolveTimelineAssetDrop,
|
|
shouldHandleTimelineDeleteKey,
|
|
getDefaultDroppedTrack,
|
|
} from "./timelineLayout";
|
|
export { formatTimelineTickLabel, generateTicks } from "./timelineRulerGeometry";
|
|
|
|
export {
|
|
getTimelineScrollTopForGeometryChange,
|
|
getTimelineVisibleTimeRange,
|
|
} from "./timelineViewportGeometry";
|
|
|
|
export const Timeline = memo(function Timeline({
|
|
onSeek,
|
|
onDrillDown,
|
|
renderClipContent,
|
|
renderClipOverlay,
|
|
onFileDrop,
|
|
onAssetDrop,
|
|
onBlockDrop,
|
|
onCompositionDrop,
|
|
onDeleteElement: _onDeleteElement,
|
|
onMoveElement: onMoveElementOverride,
|
|
onMoveElements: onMoveElementsOverride,
|
|
onResizeElement: onResizeElementOverride,
|
|
onResizeElements: onResizeElementsOverride,
|
|
onBlockedEditAttempt: onBlockedEditAttemptOverride,
|
|
onSplitElement: onSplitElementOverride,
|
|
onSelectElement,
|
|
theme: themeOverrides,
|
|
sessionEpoch = 0,
|
|
}: TimelineProps = {}) {
|
|
const {
|
|
onMoveElement,
|
|
onMoveElements,
|
|
onResizeElement,
|
|
onResizeElements,
|
|
onBlockedEditAttempt,
|
|
onSplitElement,
|
|
onRazorSplitAll,
|
|
onDeleteKeyframe,
|
|
onDeleteAllKeyframes,
|
|
onMoveKeyframeToPlayhead,
|
|
onMoveKeyframe,
|
|
} = useResolvedTimelineEditCallbacks({
|
|
onMoveElement: onMoveElementOverride,
|
|
onMoveElements: onMoveElementsOverride,
|
|
onResizeElement: onResizeElementOverride,
|
|
onResizeElements: onResizeElementsOverride,
|
|
onBlockedEditAttempt: onBlockedEditAttemptOverride,
|
|
onSplitElement: onSplitElementOverride,
|
|
});
|
|
const theme = useMemo(() => ({ ...defaultTimelineTheme, ...themeOverrides }), [themeOverrides]);
|
|
const refreshAfterLaneMove = useTimelineLaneMoveRefresh();
|
|
useMusicBeatAnalysis();
|
|
const rawElements = usePlayerStore((s) => s.elements);
|
|
const expandedElements = useExpandedTimelineElements();
|
|
const beatAnalysis = usePlayerStore((s) => s.beatAnalysis);
|
|
const musicElement = usePlayerStore((s) => getTimelineElementIndexes(s.elements).musicElement);
|
|
const beatEdits = usePlayerStore((s) => s.beatEdits);
|
|
const adjustedBeatAnalysis = useMemo(
|
|
() => remapBeatAnalysisToComposition(beatAnalysis, musicElement, beatEdits),
|
|
[beatAnalysis, musicElement, beatEdits],
|
|
);
|
|
const duration = usePlayerStore((s) => s.duration);
|
|
const timeDisplayMode = usePlayerStore((s) => s.timeDisplayMode);
|
|
const timelineReady = usePlayerStore((s) => s.timelineReady);
|
|
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
|
const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
|
|
const clipRevealRequest = usePlayerStore((s) => s.clipRevealRequest);
|
|
const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
|
|
const gsapAnimations = usePlayerStore((s) => s.gsapAnimations);
|
|
const labelMode = useMemo(() => hasKeyframedTimelineClips(gsapAnimations), [gsapAnimations]);
|
|
// The label column provides pre-t=0 space; otherwise keep TRACKS_LEFT_PAD after the gutter.
|
|
const contentOrigin = labelMode ? LABEL_COL_W + GUTTER : GUTTER + TRACKS_LEFT_PAD;
|
|
const contentGutter = labelMode ? GUTTER : 0;
|
|
const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
|
|
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
const { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent } = useTimelineZoom();
|
|
const playheadRef = useRef<HTMLDivElement>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const activeTool = usePlayerStore((s) => s.activeTool);
|
|
const [hoveredClip, setHoveredClip] = useState<string | null>(null);
|
|
const isDragging = useRef(false);
|
|
const shiftHeld = useTimelineShiftModifier();
|
|
const [showPopover, setShowPopover] = useState(false);
|
|
const [kfContextMenu, setKfContextMenu] = useState<KeyframeDiamondContextMenuState | null>(null);
|
|
const [clipContextMenu, setClipContextMenu] = useState<ClipContextMenuState | null>(null);
|
|
const setContainerRef = useCallback((el: HTMLDivElement | null) => {
|
|
containerRef.current = el;
|
|
}, []);
|
|
const lastScrollLeftRef = useRef(0);
|
|
const effectiveDuration = useMemo(
|
|
() => getEffectiveTimelineDuration(duration, rawElements),
|
|
[duration, rawElements],
|
|
);
|
|
const keyframeCache = usePlayerStore((s) => s.keyframeCache);
|
|
useAutoExpandKeyframedClips(gsapAnimations);
|
|
const {
|
|
tracks,
|
|
trackStyles,
|
|
trackOrder,
|
|
trackOrderRef,
|
|
laneCounts,
|
|
rowGeometry,
|
|
rowGeometryRef,
|
|
} = useTimelineTrackLayout(
|
|
expandedElements,
|
|
gsapAnimations,
|
|
selectedElementId,
|
|
selectedElementIds,
|
|
);
|
|
const expandedElementsRef = useRef(expandedElements);
|
|
expandedElementsRef.current = expandedElements;
|
|
const ppsRef = useRef(100);
|
|
const durationRef = useRef(effectiveDuration);
|
|
durationRef.current = effectiveDuration;
|
|
// Declared before the fitPps derivation so the edit-pin wrappers can close over it.
|
|
const fitPpsRef = useRef(100);
|
|
const {
|
|
pinZoomBeforeEdit,
|
|
setRangeSelectionRef,
|
|
pinnedOnMoveElement,
|
|
pinnedOnMoveElements,
|
|
pinnedOnResizeElement,
|
|
pinnedOnResizeElements,
|
|
pinnedOnFileDrop,
|
|
pinnedOnAssetDrop,
|
|
pinnedOnBlockDrop,
|
|
pinnedOnCompositionDrop,
|
|
} = useTimelineEditPinning({
|
|
ppsRef,
|
|
fitPpsRef,
|
|
onMoveElement,
|
|
onMoveElements,
|
|
onResizeElement,
|
|
onResizeElements,
|
|
onFileDrop,
|
|
onAssetDrop,
|
|
onBlockDrop,
|
|
onCompositionDrop,
|
|
});
|
|
const { readClipZIndex, applyStackingPatches, zSyncEnabled } = useTimelineStackingSync({
|
|
expandedElementsRef,
|
|
});
|
|
const {
|
|
gapMenuModel,
|
|
gapHighlight,
|
|
setHoveredGapAction,
|
|
openGapMenu,
|
|
dismissGapMenu,
|
|
closeTrackGap,
|
|
closeAllTrackGaps,
|
|
} = useTrackGapMenu({
|
|
tracks,
|
|
expandedElementsRef,
|
|
trackOrderRef,
|
|
onMoveElement: pinnedOnMoveElement,
|
|
onMoveElements: pinnedOnMoveElements,
|
|
});
|
|
|
|
const {
|
|
draggedClip,
|
|
setDraggedClip,
|
|
resizingClip,
|
|
setResizingClip,
|
|
blockedClipRef,
|
|
suppressClickRef,
|
|
} = useTimelineClipDrag({
|
|
scrollRef,
|
|
ppsRef,
|
|
durationRef,
|
|
trackOrderRef,
|
|
rowGeometryRef,
|
|
onMoveElement: pinnedOnMoveElement,
|
|
onMoveElements: pinnedOnMoveElements,
|
|
onResizeElement: pinnedOnResizeElement,
|
|
onResizeElements: pinnedOnResizeElements,
|
|
onBlockedEditAttempt,
|
|
setShowPopover,
|
|
setRangeSelectionRef,
|
|
readZIndex: zSyncEnabled ? readClipZIndex : undefined,
|
|
onStackingPatches: zSyncEnabled ? applyStackingPatches : undefined,
|
|
refreshAfterLaneMove,
|
|
sessionEpoch,
|
|
});
|
|
|
|
const assetDrop = useTimelineAssetDrop({
|
|
scrollRef,
|
|
ppsRef,
|
|
durationRef,
|
|
trackOrderRef,
|
|
rowGeometryRef,
|
|
contentOrigin,
|
|
onFileDrop: pinnedOnFileDrop,
|
|
onAssetDrop: pinnedOnAssetDrop,
|
|
onBlockDrop: pinnedOnBlockDrop,
|
|
onCompositionDrop: pinnedOnCompositionDrop,
|
|
sessionEpoch,
|
|
});
|
|
const displayLayout = useTimelineDisplayLayout(draggedClip, trackOrder, rowGeometry);
|
|
const resizingElementIds =
|
|
resizingClip?.groupPreview?.map((change) => change.key) ??
|
|
(resizingClip ? [getTimelineElementIdentity(resizingClip.element)] : undefined);
|
|
const { recordTimelineScroll } = useTimelinePerformanceTelemetry({
|
|
totalClipCount: expandedElements.length,
|
|
totalRowCount: displayLayout.displayTrackOrder.length,
|
|
zoomMode,
|
|
});
|
|
const { viewport, showShortcutHint, setScrollRef, syncScrollViewport } =
|
|
useTimelineScrollViewport(scrollRef, [
|
|
timelineReady,
|
|
expandedElements.length,
|
|
displayLayout.totalH,
|
|
]);
|
|
const rowWindow = useTimelineRowVirtualization({
|
|
scrollRef,
|
|
viewport,
|
|
rowGeometry: displayLayout.rowGeometry,
|
|
sessionEpoch,
|
|
elements: expandedElements,
|
|
selectedElementId,
|
|
revealElementId: clipRevealRequest?.elementId ?? null,
|
|
draggedRowKey: draggedClip?.started ? draggedClip.previewTrack : undefined,
|
|
resizingElementIds,
|
|
clipContextMenuRowKey: clipContextMenu?.element.track,
|
|
keyframeContextMenuRowKey: kfContextMenu?.element.track,
|
|
lastScrollLeftRef,
|
|
syncScrollViewport,
|
|
});
|
|
const { enabled: rowVirtualizationActive, virtualRows, focusedElementId } = rowWindow;
|
|
const selectedKeyframes = usePlayerStore((s) => s.selectedKeyframes);
|
|
const toggleSelectedKeyframe = usePlayerStore((s) => s.toggleSelectedKeyframe);
|
|
const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
|
|
useTimelineKeyframeHandlers({
|
|
expandedElements,
|
|
keyframeCache,
|
|
onSelectElement,
|
|
onSeek,
|
|
setSelectedElementId,
|
|
setKfContextMenu,
|
|
toggleSelectedKeyframe,
|
|
});
|
|
const { pps, fitPps, displayContentWidth, displayDuration, zoomModeRef, manualZoomPercentRef } =
|
|
useTimelineGeometry({
|
|
viewportWidth: viewport.clientWidth,
|
|
effectiveDuration,
|
|
zoomMode,
|
|
manualZoomPercent,
|
|
ppsRef,
|
|
fitPpsRef,
|
|
draggedClip,
|
|
resizingClip,
|
|
expandedElements,
|
|
isDragging,
|
|
scrollRef,
|
|
lastScrollLeftRef,
|
|
contentOrigin,
|
|
});
|
|
const { clipIndex, renderTimeRange, pinnedClipIdentities } = useTimelineClipRenderWindow({
|
|
tracks,
|
|
viewport,
|
|
pixelsPerSecond: pps,
|
|
contentOrigin,
|
|
duration: displayDuration,
|
|
selectedElementId: selectedElementId ?? undefined,
|
|
draggedElementId: draggedClip ? getTimelineElementIdentity(draggedClip.element) : undefined,
|
|
resizingElementIds,
|
|
revealElementId: clipRevealRequest?.elementId,
|
|
focusedEaseElementId: focusedEaseSegment?.elementId,
|
|
clipContextMenuElementId: clipContextMenu
|
|
? getTimelineElementIdentity(clipContextMenu.element)
|
|
: undefined,
|
|
keyframeContextMenuElementId: kfContextMenu
|
|
? getTimelineElementIdentity(kfContextMenu.element)
|
|
: undefined,
|
|
focusedElementId,
|
|
scrollRef,
|
|
elements: expandedElements,
|
|
rowGeometry: displayLayout.rowGeometry,
|
|
allowHorizontalReveal: zoomMode === "manual",
|
|
rowVirtualizationActive,
|
|
sessionEpoch,
|
|
});
|
|
useTimelineActiveClips({
|
|
scrollRef,
|
|
currentTime,
|
|
clipStateVersion: renderTimeRange,
|
|
elementStateVersion: expandedElements,
|
|
});
|
|
const laneGapStrips = useTimelineGapHighlights({
|
|
gapHighlight,
|
|
tracks,
|
|
selectedElementId,
|
|
selectedElementIds,
|
|
expandedElements,
|
|
dragActive: draggedClip?.started === true || resizingClip != null,
|
|
displayDuration,
|
|
});
|
|
|
|
const { seekFromX, autoScrollDuringDrag, dragScrollRaf } = useTimelinePlayhead({
|
|
playheadRef,
|
|
scrollRef,
|
|
ppsRef,
|
|
durationRef,
|
|
isDragging,
|
|
currentTime,
|
|
zoomMode,
|
|
manualZoomPercent,
|
|
zoomModeRef,
|
|
manualZoomPercentRef,
|
|
fitPps,
|
|
fitPpsRef,
|
|
effectiveDuration,
|
|
pps,
|
|
timelineReady,
|
|
elementsLength: expandedElements.length,
|
|
setZoomMode,
|
|
setManualZoomPercent,
|
|
onSeek,
|
|
contentOrigin,
|
|
});
|
|
const { razorGuideX, updateRazorGuide, clearRazorGuide, splitAllAtPointer } =
|
|
useTimelineRazorInteraction({
|
|
active: activeTool === "razor",
|
|
scrollRef,
|
|
contentOrigin,
|
|
pixelsPerSecond: pps,
|
|
onSplitAll: onRazorSplitAll,
|
|
});
|
|
|
|
const {
|
|
rangeSelection,
|
|
setRangeSelection,
|
|
shiftClickClipRef,
|
|
marqueeRect,
|
|
isScrubbing,
|
|
handlePointerDown,
|
|
handlePointerMove,
|
|
handlePointerUp,
|
|
handlePointerCancel,
|
|
} = useTimelineRangeSelection({
|
|
scrollRef,
|
|
ppsRef,
|
|
effectiveDuration,
|
|
pps,
|
|
onSeek,
|
|
seekFromX,
|
|
autoScrollDuringDrag,
|
|
dragScrollRaf,
|
|
isDragging,
|
|
setShowPopover,
|
|
elementsRef: expandedElementsRef,
|
|
clipIndex,
|
|
rowGeometryRef,
|
|
onSelectElement,
|
|
contentOrigin,
|
|
sessionEpoch,
|
|
});
|
|
setRangeSelectionRef.current = setRangeSelection; // stable ref consumed by useTimelineClipDrag
|
|
|
|
useTimelineSelectionLifecycle(expandedElements, selectedElementId, setShowPopover, () =>
|
|
setRangeSelection(null),
|
|
);
|
|
|
|
const { major, minor, majorTickInterval } = useTimelineTicks(
|
|
displayDuration,
|
|
pps,
|
|
timeDisplayMode,
|
|
rowVirtualizationActive ? renderTimeRange : undefined,
|
|
);
|
|
|
|
const getPreviewElement = useCallback(
|
|
(element: TimelineElement): TimelineElement => getTimelinePreviewElement(element, resizingClip),
|
|
[resizingClip],
|
|
);
|
|
|
|
if (!timelineReady || expandedElements.length === 0) {
|
|
return (
|
|
<TimelineEmptyState
|
|
isDragOver={assetDrop.isDragOver}
|
|
onFileDrop={!!onFileDrop}
|
|
onDragOver={assetDrop.handleAssetDragOver}
|
|
onDragLeave={assetDrop.handleAssetDragLeave}
|
|
onDrop={assetDrop.handleAssetDrop}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={setContainerRef}
|
|
aria-label="Timeline"
|
|
data-timeline-element-count={expandedElements.length}
|
|
className={`relative border-t select-none h-full overflow-hidden ${assetDrop.isDragOver ? "ring-1 ring-inset ring-studio-accent/60" : ""} ${activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
|
|
onMouseMove={updateRazorGuide}
|
|
onMouseLeave={clearRazorGuide}
|
|
style={{
|
|
touchAction: "pan-x pan-y",
|
|
background: theme.shellBackground,
|
|
borderColor: theme.shellBorder,
|
|
}}
|
|
>
|
|
<div
|
|
ref={setScrollRef}
|
|
// Stable owner for gestures that must survive virtual row/clip unmounts.
|
|
data-timeline-scroll-viewport
|
|
data-timeline-auto-scroll-left-inset={labelMode ? LABEL_COL_W : 0}
|
|
tabIndex={-1}
|
|
className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-none`}
|
|
onScroll={(e) => {
|
|
lastScrollLeftRef.current = e.currentTarget.scrollLeft; // restored across post-edit reload
|
|
recordTimelineScroll(e.currentTarget);
|
|
syncScrollViewport(e.currentTarget, true);
|
|
}}
|
|
{...rowWindow.timelineFocusProps}
|
|
onDragOver={assetDrop.handleAssetDragOver}
|
|
onDragLeave={assetDrop.handleAssetDragLeave}
|
|
onDrop={assetDrop.handleAssetDrop}
|
|
onPointerDown={(e) => {
|
|
// Let interactive controls (keyframe nav/toggle, caret, inputs) handle
|
|
// their own clicks — scrubbing here would preventDefault and eat them.
|
|
if (e.target instanceof Element && e.target.closest("button, input, select, a")) return;
|
|
if (splitAllAtPointer(e)) return;
|
|
handlePointerDown(e);
|
|
}}
|
|
onPointerMove={handlePointerMove}
|
|
onPointerUp={handlePointerUp}
|
|
onPointerCancel={handlePointerCancel}
|
|
onLostPointerCapture={handlePointerCancel}
|
|
>
|
|
<TimelineCanvas
|
|
major={major}
|
|
minor={minor}
|
|
pps={pps}
|
|
contentOrigin={contentOrigin}
|
|
contentGutter={contentGutter}
|
|
trackContentWidth={displayContentWidth}
|
|
totalH={displayLayout.totalH}
|
|
effectiveDuration={effectiveDuration}
|
|
majorTickInterval={majorTickInterval}
|
|
rangeSelection={rangeSelection}
|
|
marqueeRect={marqueeRect}
|
|
laneGapStrips={laneGapStrips}
|
|
theme={theme}
|
|
displayTrackOrder={displayLayout.displayTrackOrder}
|
|
rowHeights={displayLayout.displayRowHeights}
|
|
rowGeometry={displayLayout.rowGeometry}
|
|
virtualRows={virtualRows}
|
|
rowsVirtualized={rowVirtualizationActive}
|
|
clipIndex={clipIndex}
|
|
renderTimeRange={renderTimeRange}
|
|
pinnedClipIdentities={pinnedClipIdentities}
|
|
trackOrder={trackOrder}
|
|
tracks={tracks}
|
|
trackStyles={trackStyles}
|
|
laneCounts={laneCounts}
|
|
selectedElementId={selectedElementId}
|
|
selectedElementIds={selectedElementIds}
|
|
hoveredClip={hoveredClip}
|
|
draggedClip={draggedClip}
|
|
resizingClip={resizingClip}
|
|
isScrubbing={isScrubbing}
|
|
blockedClipRef={blockedClipRef}
|
|
suppressClickRef={suppressClickRef}
|
|
scrollRef={scrollRef}
|
|
// Windowing drops content to mount a row cheaply; unvirtualized it is pure cost.
|
|
renderClipContent={
|
|
rowVirtualizationActive && viewport.isScrolling ? undefined : renderClipContent
|
|
}
|
|
renderClipOverlay={renderClipOverlay}
|
|
playheadRef={playheadRef}
|
|
onDrillDown={onDrillDown}
|
|
onSelectElement={onSelectElement}
|
|
setHoveredClip={setHoveredClip}
|
|
setShowPopover={setShowPopover}
|
|
setRangeSelection={setRangeSelection}
|
|
setResizingClip={setResizingClip}
|
|
setDraggedClip={setDraggedClip}
|
|
setSelectedElementId={setSelectedElementId}
|
|
shiftClickClipRef={shiftClickClipRef}
|
|
getPreviewElement={getPreviewElement}
|
|
getTrackStyle={getTrackStyle}
|
|
keyframeCache={keyframeCache}
|
|
gsapAnimations={gsapAnimations}
|
|
selectedKeyframes={selectedKeyframes}
|
|
currentTime={currentTime}
|
|
onSeek={onSeek}
|
|
beatAnalysis={adjustedBeatAnalysis}
|
|
onSelectSegment={onSelectSegment}
|
|
onClickKeyframe={onClickKeyframe}
|
|
onShiftClickKeyframe={onShiftClickKeyframe}
|
|
onMoveKeyframe={onMoveKeyframe}
|
|
onContextMenuKeyframe={onContextMenuKeyframe}
|
|
onContextMenuClip={(e, el) => {
|
|
e.preventDefault();
|
|
setSelectedElementId(el.key ?? el.id);
|
|
onSelectElement?.(el);
|
|
dismissGapMenu();
|
|
setClipContextMenu({
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
element: el,
|
|
sessionEpoch: usePlayerStore.getState().timelineSessionEpoch,
|
|
});
|
|
}}
|
|
onContextMenuLane={(e, track, time) => {
|
|
if (draggedClip?.started || resizingClip) return;
|
|
setClipContextMenu(null);
|
|
openGapMenu({ x: e.clientX, y: e.clientY, track, time });
|
|
}}
|
|
/>
|
|
{activeTool === "razor" && razorGuideX !== null && <TimelineRazorGuide x={razorGuideX} />}
|
|
</div>
|
|
<TimelineOverlays
|
|
elements={expandedElements}
|
|
elementsRef={expandedElementsRef}
|
|
theme={theme}
|
|
showShortcutHint={showShortcutHint}
|
|
showPopover={showPopover}
|
|
rangeSelection={rangeSelection}
|
|
setShowPopover={setShowPopover}
|
|
setRangeSelection={setRangeSelection}
|
|
kfContextMenu={kfContextMenu}
|
|
setKfContextMenu={setKfContextMenu}
|
|
onDeleteKeyframe={onDeleteKeyframe}
|
|
onDeleteAllKeyframes={onDeleteAllKeyframes}
|
|
onMoveKeyframeToPlayhead={onMoveKeyframeToPlayhead}
|
|
clipContextMenu={clipContextMenu}
|
|
setClipContextMenu={setClipContextMenu}
|
|
currentTime={currentTime}
|
|
onSplitElement={onSplitElement}
|
|
pinZoomBeforeEdit={pinZoomBeforeEdit}
|
|
onDeleteElement={_onDeleteElement}
|
|
gapContextMenu={gapMenuModel}
|
|
onDismissGapContextMenu={dismissGapMenu}
|
|
onCloseTrackGap={closeTrackGap}
|
|
onCloseAllTrackGaps={closeAllTrackGaps}
|
|
onHoverGapAction={setHoveredGapAction}
|
|
/>
|
|
</div>
|
|
);
|
|
});
|