+
{label}
diff --git a/packages/studio/src/player/components/Timeline.tsx b/packages/studio/src/player/components/Timeline.tsx
index 826d369698..3e1eafea95 100644
--- a/packages/studio/src/player/components/Timeline.tsx
+++ b/packages/studio/src/player/components/Timeline.tsx
@@ -61,7 +61,6 @@ export {
getTimelineScrollTopForGeometryChange,
getTimelineVisibleTimeRange,
} from "./timelineViewportGeometry";
-
export const Timeline = memo(function Timeline({
onSeek,
onDrillDown,
@@ -310,23 +309,23 @@ export const Timeline = memo(function Timeline({
setKfContextMenu,
toggleSelectedKeyframe,
});
-
- const { clipIndex, renderTimeRange, pinnedClipIdentities } = useTimelineClipRenderWindow({
- tracks,
- viewport,
- pixelsPerSecond: pps,
- contentOrigin,
- duration: displayDuration,
- selectedElementId: selectedElementId ?? undefined,
- draggedElementId: draggedClip?.element.key ?? draggedClip?.element.id,
- resizingElementIds:
- resizingClip?.groupPreview?.map((change) => change.key) ??
- (resizingClip ? [resizingClip.element.key ?? resizingClip.element.id] : undefined),
- focusedElementId: pinnedElementId,
- focusedEaseElementId: focusedEaseSegment?.elementId,
- clipContextMenuElementId: clipContextMenu?.element.key ?? clipContextMenu?.element.id,
- keyframeContextMenuElementId: kfContextMenu?.element.key ?? kfContextMenu?.element.id,
- });
+ const { clipIndex, renderTimeRange, visibleTimeRange, pinnedClipIdentities } =
+ useTimelineClipRenderWindow({
+ tracks,
+ viewport,
+ pixelsPerSecond: pps,
+ contentOrigin,
+ duration: displayDuration,
+ selectedElementId: selectedElementId ?? undefined,
+ draggedElementId: draggedClip?.element.key ?? draggedClip?.element.id,
+ resizingElementIds:
+ resizingClip?.groupPreview?.map((change) => change.key) ??
+ (resizingClip ? [resizingClip.element.key ?? resizingClip.element.id] : undefined),
+ focusedElementId: pinnedElementId,
+ focusedEaseElementId: focusedEaseSegment?.elementId,
+ clipContextMenuElementId: clipContextMenu?.element.key ?? clipContextMenu?.element.id,
+ keyframeContextMenuElementId: kfContextMenu?.element.key ?? kfContextMenu?.element.id,
+ });
useTimelineActiveClips({
scrollRef,
currentTime,
@@ -499,6 +498,7 @@ export const Timeline = memo(function Timeline({
rowsVirtualized={rowVirtualizationActive}
clipIndex={clipIndex}
renderTimeRange={renderTimeRange}
+ visibleTimeRange={visibleTimeRange}
pinnedClipIdentities={pinnedClipIdentities}
trackOrder={trackOrder}
tracks={tracks}
diff --git a/packages/studio/src/player/components/TimelineGestureOverlay.tsx b/packages/studio/src/player/components/TimelineGestureOverlay.tsx
index 500d2c0ea2..3490f30abf 100644
--- a/packages/studio/src/player/components/TimelineGestureOverlay.tsx
+++ b/packages/studio/src/player/components/TimelineGestureOverlay.tsx
@@ -9,6 +9,7 @@ import { getTimelineDragOverlayPosition } from "./timelineClipDragPreview";
import type { DraggedClipState } from "./timelineClipDragTypes";
import type { TrackVisualStyle } from "./timelineIcons";
import { isTimelineClipActive } from "./useTimelineActiveClips";
+import type { TimelineClipRenderContext } from "./TimelineTypes";
interface TimelineGestureOverlayProps {
drag: DraggedClipState | null;
@@ -22,6 +23,7 @@ interface TimelineGestureOverlayProps {
renderClipContent?: (
element: TimelineElement,
style: { clip: string; label: string },
+ context: TimelineClipRenderContext,
) => ReactNode;
renderClipOverlay?: (element: TimelineElement) => ReactNode;
}
@@ -87,6 +89,7 @@ export const TimelineGestureOverlay = memo(function TimelineGestureOverlay({
getTrackStyle(element.tag),
renderClipContent,
renderClipOverlay,
+ { priority: "interaction", rich: true },
)}
diff --git a/packages/studio/src/player/components/TimelineLaneTypes.ts b/packages/studio/src/player/components/TimelineLaneTypes.ts
index aa2157c3a6..44b18a9228 100644
--- a/packages/studio/src/player/components/TimelineLaneTypes.ts
+++ b/packages/studio/src/player/components/TimelineLaneTypes.ts
@@ -10,6 +10,7 @@ import type { TimelineClipIndex, TimelineTimeRange } from "../lib/timelineClipIn
import type { TimelineRowGeometry } from "./timelineLayout";
import type { TimelineVirtualRow } from "./useTimelineVirtualRows";
import type { TimelineLogicalRow } from "./timelineKeyboardNavigation";
+import type { TimelineClipRenderContext } from "./TimelineTypes";
/** Props shared by TimelineCanvas and its lane renderer. */
export interface TimelineLaneBaseProps {
@@ -27,6 +28,7 @@ export interface TimelineLaneBaseProps {
rowsVirtualized: boolean;
clipIndex: TimelineClipIndex;
renderTimeRange: TimelineTimeRange;
+ visibleTimeRange: TimelineTimeRange;
pinnedClipIdentities: ReadonlySet
;
trackOrder: number[];
tracks: [number, TimelineElement[]][];
@@ -42,6 +44,7 @@ export interface TimelineLaneBaseProps {
renderClipContent?: (
element: TimelineElement,
style: { clip: string; label: string },
+ context: TimelineClipRenderContext,
) => ReactNode;
renderClipOverlay?: (element: TimelineElement) => ReactNode;
onDrillDown?: (element: TimelineElement) => void;
diff --git a/packages/studio/src/player/components/TimelineLanes.tsx b/packages/studio/src/player/components/TimelineLanes.tsx
index 33be905d00..19675490d7 100644
--- a/packages/studio/src/player/components/TimelineLanes.tsx
+++ b/packages/studio/src/player/components/TimelineLanes.tsx
@@ -26,6 +26,7 @@ import { queryTimelineClipIndex } from "../lib/timelineClipIndex";
import type { TimelineLogicalRow } from "./timelineKeyboardNavigation";
import { timelineClipFocusId } from "./timelineNavigationIdentity";
import { useTimelineKeyboardActor } from "./useTimelineKeyboardActor";
+import type { TimelineClipRenderContext } from "./TimelineTypes";
interface TimelineLanesProps extends TimelineLaneBaseProps {
/** Live-derived by TimelineCanvas from {@link TimelineLaneBaseProps.draggedClip}. */
@@ -53,6 +54,7 @@ export function TimelineLanes({
rowsVirtualized,
clipIndex,
renderTimeRange,
+ visibleTimeRange,
pinnedClipIdentities,
trackOrder,
tracks,
@@ -170,11 +172,7 @@ export function TimelineLanes({
const ts = trackStyles.get(trackNum) ?? getTrackStyle("");
const isPendingTrack =
draggedClip?.started === true && !trackOrder.includes(trackNum) && els.length === 0;
- // All lanes use the same uniform color — no alternating stripes.
const rowBackground = theme.rowBackground;
- // The beat-dot strip occupies the top of this track's lane (active track,
- // or the music track when nothing is selected). When shown, keyframe
- // diamonds shrink + drop to the bottom half so they don't collide with it.
const beatStripOnTrack =
(beatAnalysis?.beatTimes?.length ?? 0) >= 2 &&
(selectedElementId
@@ -182,9 +180,6 @@ export function TimelineLanes({
: els.some(isMusicTrack));
const isTrackHidden = els.length > 0 && els.every((element) => element.hidden === true);
const isAudioTrack = els.length > 0 && els.some(isAudioTimelineElement);
- // The one keyframed element this track shows lanes for (selected, else
- // most lanes). A track can hold several elements; scoping to one keeps
- // their keyframes from cramming into a single row.
const keyframeClip = STUDIO_KEYFRAMES_ENABLED
? resolveTrackKeyframeClip(els, laneCounts, selectedElementId, selectedElementIds)
: null;
@@ -241,9 +236,6 @@ export function TimelineLanes({
}}
className="relative"
onContextMenu={(e: React.MouseEvent) => {
- // Clip / keyframe-diamond context menus preventDefault at the
- // target before this bubble handler runs — respect them so a
- // right-click on a clip never also opens the gap menu.
if (e.defaultPrevented || !onContextMenuLane) return;
const rect = e.currentTarget.getBoundingClientRect();
const time = (e.clientX - rect.left) / pps;
@@ -295,9 +287,6 @@ export function TimelineLanes({
renderElements.map((el) => {
const clipStyle = getTrackStyle(el.tag);
const elementKey = el.key ?? el.id;
- // Only the track's active keyframe clip shows expanded lanes;
- // other clips (incl. siblings on a shared track) show compact
- // diamonds on their own bar instead.
const showsLanes =
STUDIO_KEYFRAMES_ENABLED &&
elementKey === keyframeClipKey &&
@@ -312,6 +301,19 @@ export function TimelineLanes({
(draggedElement?.key ?? draggedElement?.id) === elementKey;
if (isDraggingClip) return null;
const previewElement = getPreviewElement(el);
+ const isInteractive =
+ isSelected || hoveredClip === clipKey || pinnedClipIdentities.has(clipKey);
+ const intersectsVisible =
+ previewElement.start < visibleTimeRange.end &&
+ previewElement.start + previewElement.duration > visibleTimeRange.start;
+ const renderContext: TimelineClipRenderContext = {
+ priority: isInteractive
+ ? "interaction"
+ : intersectsVisible
+ ? "visible"
+ : "overscan",
+ rich: isInteractive,
+ };
const isPassenger =
multiDragPreview != null && isMultiDragPassenger(clipKey, multiDragPreview);
const passengerOffsetPx = isPassenger
@@ -475,6 +477,7 @@ export function TimelineLanes({
clipStyle,
renderClipContent,
renderClipOverlay,
+ renderContext,
)}
);
diff --git a/packages/studio/src/player/components/TimelineTypes.ts b/packages/studio/src/player/components/TimelineTypes.ts
index 0838e8dccb..2908f026e6 100644
--- a/packages/studio/src/player/components/TimelineTypes.ts
+++ b/packages/studio/src/player/components/TimelineTypes.ts
@@ -4,6 +4,11 @@ import type { TimelineDropCallbacks } from "./timelineCallbacks";
import type { TimelineTheme } from "./timelineTheme";
import type { TimelineEditOverrides } from "./useResolvedTimelineEditCallbacks";
+export interface TimelineClipRenderContext {
+ priority: "overscan" | "visible" | "interaction";
+ rich: boolean;
+}
+
export interface TimelineProps extends TimelineDropCallbacks, TimelineEditOverrides {
/** Project-scoped reset boundary; soft source refreshes retain the same epoch. */
sessionEpoch?: number;
@@ -12,6 +17,7 @@ export interface TimelineProps extends TimelineDropCallbacks, TimelineEditOverri
renderClipContent?: (
element: TimelineElement,
style: { clip: string; label: string },
+ context: TimelineClipRenderContext,
) => ReactNode;
renderClipOverlay?: (element: TimelineElement) => ReactNode;
onDeleteElement?: (element: TimelineElement) => Promise | void;
diff --git a/packages/studio/src/player/components/timelineClipChildren.tsx b/packages/studio/src/player/components/timelineClipChildren.tsx
index 3482fbdc00..b5d14d20fb 100644
--- a/packages/studio/src/player/components/timelineClipChildren.tsx
+++ b/packages/studio/src/player/components/timelineClipChildren.tsx
@@ -1,6 +1,7 @@
import { type ReactNode } from "react";
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
import type { TrackVisualStyle } from "./timelineIcons";
+import type { TimelineClipRenderContext } from "./TimelineTypes";
function ClipLintDot({ element }: { element: TimelineElement }) {
const lint = usePlayerStore((s) => s.lintFindingsByElement.get(element.key ?? element.id));
@@ -18,9 +19,14 @@ export function renderClipChildren(
element: TimelineElement,
clipStyle: TrackVisualStyle,
renderClipContent:
- | ((element: TimelineElement, style: { clip: string; label: string }) => ReactNode)
+ | ((
+ element: TimelineElement,
+ style: { clip: string; label: string },
+ context: TimelineClipRenderContext,
+ ) => ReactNode)
| undefined,
renderClipOverlay: ((element: TimelineElement) => ReactNode) | undefined,
+ context: TimelineClipRenderContext = { priority: "visible", rich: false },
): ReactNode {
return (
<>
@@ -31,7 +37,7 @@ export function renderClipChildren(
// diamonds hang outside its bounds), so the thumbnail layer must clip
// itself to the clip's rounded corners or sharp corners poke out.
- {renderClipContent(element, clipStyle)}
+ {renderClipContent(element, clipStyle, context)}
)}
>
diff --git a/packages/studio/src/player/components/useTimelineClipRenderWindow.ts b/packages/studio/src/player/components/useTimelineClipRenderWindow.ts
index b173dc2217..3b534b8541 100644
--- a/packages/studio/src/player/components/useTimelineClipRenderWindow.ts
+++ b/packages/studio/src/player/components/useTimelineClipRenderWindow.ts
@@ -1,6 +1,9 @@
import { useMemo } from "react";
import { createTimelineClipIndex } from "../lib/timelineClipIndex";
-import { getTimelineRenderTimeRange } from "./timelineViewportGeometry";
+import {
+ getTimelineRenderTimeRange,
+ getTimelineVisibleTimeRange,
+} from "./timelineViewportGeometry";
import type { TimelineScrollViewportSnapshot } from "./useTimelineScrollViewport";
interface UseTimelineClipRenderWindowInput {
@@ -37,6 +40,10 @@ export function useTimelineClipRenderWindow({
() => getTimelineRenderTimeRange(viewport, pixelsPerSecond, contentOrigin, duration),
[contentOrigin, duration, pixelsPerSecond, viewport],
);
+ const visibleTimeRange = useMemo(
+ () => getTimelineVisibleTimeRange(viewport, pixelsPerSecond, contentOrigin, duration),
+ [contentOrigin, duration, pixelsPerSecond, viewport],
+ );
const pinnedClipIdentities = useMemo(
() =>
new Set(
@@ -60,5 +67,5 @@ export function useTimelineClipRenderWindow({
selectedElementId,
],
);
- return { clipIndex, renderTimeRange, pinnedClipIdentities };
+ return { clipIndex, renderTimeRange, visibleTimeRange, pinnedClipIdentities };
}