@hyperframes/studio 0.6.0-alpha.9 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-hYc4aP7M.js +117 -0
  4. package/dist/favicon.svg +14 -0
  5. package/dist/index.html +3 -2
  6. package/package.json +9 -9
  7. package/src/App.tsx +421 -4303
  8. package/src/captions/components/CaptionOverlay.tsx +13 -246
  9. package/src/captions/components/CaptionOverlayUtils.ts +221 -0
  10. package/src/components/AskAgentModal.tsx +120 -0
  11. package/src/components/StudioHeader.tsx +133 -0
  12. package/src/components/StudioLeftSidebar.tsx +125 -0
  13. package/src/components/StudioPreviewArea.tsx +167 -0
  14. package/src/components/StudioRightPanel.tsx +198 -0
  15. package/src/components/TimelineToolbar.tsx +89 -0
  16. package/src/components/editor/DomEditOverlay.tsx +88 -993
  17. package/src/components/editor/EaseCurveEditor.tsx +221 -0
  18. package/src/components/editor/FileTree.tsx +13 -621
  19. package/src/components/editor/FileTreeIcons.tsx +128 -0
  20. package/src/components/editor/FileTreeNodes.tsx +496 -0
  21. package/src/components/editor/MotionPanel.tsx +16 -390
  22. package/src/components/editor/MotionPanelFields.tsx +185 -0
  23. package/src/components/editor/PropertyPanel.test.ts +0 -49
  24. package/src/components/editor/PropertyPanel.tsx +132 -2763
  25. package/src/components/editor/domEditOverlayGeometry.ts +211 -0
  26. package/src/components/editor/domEditOverlayGestures.ts +138 -0
  27. package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
  28. package/src/components/editor/domEditing.ts +44 -1117
  29. package/src/components/editor/domEditingAgentPrompt.ts +97 -0
  30. package/src/components/editor/domEditingDom.ts +266 -0
  31. package/src/components/editor/domEditingElement.ts +329 -0
  32. package/src/components/editor/domEditingLayers.ts +460 -0
  33. package/src/components/editor/domEditingTypes.ts +125 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  35. package/src/components/editor/manualEditingAvailability.ts +1 -1
  36. package/src/components/editor/manualEdits.ts +84 -1049
  37. package/src/components/editor/manualEditsDom.ts +436 -0
  38. package/src/components/editor/manualEditsParsing.ts +280 -0
  39. package/src/components/editor/manualEditsSnapshot.ts +333 -0
  40. package/src/components/editor/manualEditsTypes.ts +141 -0
  41. package/src/components/editor/propertyPanelColor.tsx +371 -0
  42. package/src/components/editor/propertyPanelFill.tsx +421 -0
  43. package/src/components/editor/propertyPanelFont.tsx +455 -0
  44. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  45. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  46. package/src/components/editor/propertyPanelSections.tsx +453 -0
  47. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  48. package/src/components/editor/studioMotion.ts +47 -434
  49. package/src/components/editor/studioMotionOps.ts +299 -0
  50. package/src/components/editor/studioMotionTypes.ts +168 -0
  51. package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
  52. package/src/components/editor/useDomEditOverlayRects.ts +207 -0
  53. package/src/components/nle/NLELayout.tsx +68 -155
  54. package/src/components/nle/NLEPreview.tsx +3 -0
  55. package/src/components/nle/useCompositionStack.ts +126 -0
  56. package/src/components/renders/RenderQueue.tsx +102 -31
  57. package/src/components/renders/useRenderQueue.ts +8 -2
  58. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  59. package/src/contexts/DomEditContext.tsx +137 -0
  60. package/src/contexts/FileManagerContext.tsx +110 -0
  61. package/src/contexts/PanelLayoutContext.tsx +68 -0
  62. package/src/contexts/StudioContext.tsx +135 -0
  63. package/src/hooks/useAppHotkeys.ts +326 -0
  64. package/src/hooks/useAskAgentModal.ts +162 -0
  65. package/src/hooks/useCaptionDetection.ts +132 -0
  66. package/src/hooks/useCompositionDimensions.ts +25 -0
  67. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  68. package/src/hooks/useDomEditCommits.ts +437 -0
  69. package/src/hooks/useDomEditSession.ts +342 -0
  70. package/src/hooks/useDomEditTextCommits.ts +330 -0
  71. package/src/hooks/useDomSelection.ts +398 -0
  72. package/src/hooks/useFileManager.ts +431 -0
  73. package/src/hooks/useFrameCapture.ts +77 -0
  74. package/src/hooks/useLintModal.ts +35 -0
  75. package/src/hooks/useManifestPersistence.ts +492 -0
  76. package/src/hooks/usePanelLayout.ts +68 -0
  77. package/src/hooks/usePreviewInteraction.ts +153 -0
  78. package/src/hooks/useRenderClipContent.ts +124 -0
  79. package/src/hooks/useTimelineEditing.ts +472 -0
  80. package/src/hooks/useToast.ts +20 -0
  81. package/src/player/components/Player.tsx +33 -2
  82. package/src/player/components/Timeline.test.ts +0 -8
  83. package/src/player/components/Timeline.tsx +196 -1518
  84. package/src/player/components/TimelineCanvas.tsx +434 -0
  85. package/src/player/components/TimelineClip.tsx +9 -244
  86. package/src/player/components/TimelineEmptyState.tsx +102 -0
  87. package/src/player/components/TimelineRuler.tsx +90 -0
  88. package/src/player/components/timelineIcons.tsx +49 -0
  89. package/src/player/components/timelineLayout.ts +215 -0
  90. package/src/player/components/timelineUtils.ts +211 -0
  91. package/src/player/components/useTimelineClipDrag.ts +388 -0
  92. package/src/player/components/useTimelinePlayhead.ts +200 -0
  93. package/src/player/components/useTimelineRangeSelection.ts +135 -0
  94. package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
  95. package/src/player/hooks/useTimelinePlayer.ts +105 -1371
  96. package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
  97. package/src/player/lib/playbackAdapter.ts +145 -0
  98. package/src/player/lib/playbackShortcuts.ts +68 -0
  99. package/src/player/lib/playbackTypes.ts +60 -0
  100. package/src/player/lib/timelineDOM.ts +373 -0
  101. package/src/player/lib/timelineElementHelpers.ts +303 -0
  102. package/src/player/lib/timelineIframeHelpers.ts +269 -0
  103. package/src/utils/domEditHelpers.ts +50 -0
  104. package/src/utils/studioFontHelpers.ts +83 -0
  105. package/src/utils/studioHelpers.ts +214 -0
  106. package/src/utils/studioPreviewHelpers.ts +185 -0
  107. package/src/utils/timelineDiscovery.ts +1 -1
  108. package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
  109. package/dist/assets/index-14zH9lqh.css +0 -1
  110. package/dist/assets/index-DYCiFGWQ.js +0 -108
  111. package/src/player/components/TimelineClip.test.ts +0 -92
@@ -0,0 +1,215 @@
1
+ import { formatTime } from "../lib/time";
2
+ import type { ZoomMode } from "../store/playerStore";
3
+
4
+ /* ── Layout constants ──────────────────────────────────────────────── */
5
+ export const GUTTER = 32;
6
+ export const TRACK_H = 72;
7
+ export const RULER_H = 24;
8
+ export const CLIP_Y = 3;
9
+ export const CLIP_HANDLE_W = 18;
10
+ export const TIMELINE_SCROLL_BUFFER = 20;
11
+
12
+ /* ── Tick generation ──────────────────────────────────────────────── */
13
+ function getMajorTickInterval(duration: number, pixelsPerSecond?: number): number {
14
+ const zoomIntervals = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600];
15
+ if (Number.isFinite(pixelsPerSecond) && (pixelsPerSecond ?? 0) > 0) {
16
+ const targetMajorPx = 128;
17
+ return (
18
+ zoomIntervals.find((interval) => interval * (pixelsPerSecond ?? 0) >= targetMajorPx) ?? 600
19
+ );
20
+ }
21
+ const durationIntervals = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60];
22
+ const target = duration / 6;
23
+ return durationIntervals.find((interval) => interval >= target) ?? 60;
24
+ }
25
+
26
+ function getMinorTickInterval(majorInterval: number, pixelsPerSecond?: number): number {
27
+ let interval = majorInterval / 2;
28
+ if (majorInterval >= 30) interval = majorInterval / 6;
29
+ else if (majorInterval >= 15) interval = majorInterval / 3;
30
+ else if (majorInterval >= 5) interval = majorInterval / 5;
31
+ else if (majorInterval >= 1) interval = majorInterval / 4;
32
+
33
+ if (
34
+ Number.isFinite(pixelsPerSecond) &&
35
+ (pixelsPerSecond ?? 0) > 0 &&
36
+ interval * (pixelsPerSecond ?? 0) < 20
37
+ ) {
38
+ return Math.max(0.25, majorInterval / 2);
39
+ }
40
+ return Math.max(0.25, interval);
41
+ }
42
+
43
+ export function generateTicks(
44
+ duration: number,
45
+ pixelsPerSecond?: number,
46
+ ): { major: number[]; minor: number[] } {
47
+ if (duration <= 0 || !Number.isFinite(duration) || duration > 7200)
48
+ return { major: [], minor: [] };
49
+ const majorInterval = getMajorTickInterval(duration, pixelsPerSecond);
50
+ const minorInterval = getMinorTickInterval(majorInterval, pixelsPerSecond);
51
+ const major: number[] = [];
52
+ const minor: number[] = [];
53
+ const maxTicks = 2000; // Safety cap to prevent runaway tick generation
54
+ for (
55
+ let t = 0;
56
+ t <= duration + 0.001 && major.length + minor.length < maxTicks;
57
+ t += minorInterval
58
+ ) {
59
+ const rounded = Math.round(t * 100) / 100;
60
+ const isMajor =
61
+ Math.abs(rounded % majorInterval) < 0.01 ||
62
+ Math.abs((rounded % majorInterval) - majorInterval) < 0.01;
63
+ if (isMajor) major.push(rounded);
64
+ else minor.push(rounded);
65
+ }
66
+ return { major, minor };
67
+ }
68
+
69
+ export function formatTimelineTickLabel(time: number, duration: number, majorInterval: number) {
70
+ if (!Number.isFinite(time)) return "0:00";
71
+ const safeTime = Math.max(0, time);
72
+ if (majorInterval < 1) {
73
+ const totalTenths = Math.round(safeTime * 10);
74
+ const wholeSeconds = Math.floor(totalTenths / 10);
75
+ const tenth = totalTenths % 10;
76
+ return `${formatTime(wholeSeconds)}.${tenth}`;
77
+ }
78
+ if (duration >= 3600 || safeTime >= 3600) {
79
+ const totalSeconds = Math.floor(safeTime);
80
+ const hours = Math.floor(totalSeconds / 3600);
81
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
82
+ const seconds = totalSeconds % 60;
83
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
84
+ }
85
+ return formatTime(safeTime);
86
+ }
87
+
88
+ /* ── Scroll / zoom helpers ────────────────────────────────────────── */
89
+ export function shouldAutoScrollTimeline(
90
+ zoomMode: ZoomMode,
91
+ scrollWidth: number,
92
+ clientWidth: number,
93
+ ): boolean {
94
+ if (zoomMode === "fit") return false;
95
+ if (!Number.isFinite(scrollWidth) || !Number.isFinite(clientWidth)) return false;
96
+ return scrollWidth - clientWidth > 1;
97
+ }
98
+
99
+ export function getTimelineScrollLeftForZoomTransition(
100
+ previousZoomMode: ZoomMode | null,
101
+ nextZoomMode: ZoomMode,
102
+ currentScrollLeft: number,
103
+ ): number {
104
+ if (previousZoomMode === "manual" && nextZoomMode === "fit") return 0;
105
+ return currentScrollLeft;
106
+ }
107
+
108
+ export function getTimelineScrollLeftForZoomAnchor(input: {
109
+ pointerX: number;
110
+ currentScrollLeft: number;
111
+ gutter: number;
112
+ currentPixelsPerSecond: number;
113
+ nextPixelsPerSecond: number;
114
+ duration: number;
115
+ }): number {
116
+ const currentPps = Math.max(0, input.currentPixelsPerSecond);
117
+ const nextPps = Math.max(0, input.nextPixelsPerSecond);
118
+ if (
119
+ !Number.isFinite(input.pointerX) ||
120
+ !Number.isFinite(input.currentScrollLeft) ||
121
+ !Number.isFinite(input.duration) ||
122
+ input.duration <= 0 ||
123
+ currentPps <= 0 ||
124
+ nextPps <= 0
125
+ ) {
126
+ return Math.max(0, input.currentScrollLeft);
127
+ }
128
+ const timelineX = Math.max(0, input.currentScrollLeft + input.pointerX - input.gutter);
129
+ const timeAtPointer = Math.max(0, Math.min(input.duration, timelineX / currentPps));
130
+ return Math.max(0, input.gutter + timeAtPointer * nextPps - input.pointerX);
131
+ }
132
+
133
+ /* ── Playhead / canvas ────────────────────────────────────────────── */
134
+ export function getTimelinePlayheadLeft(time: number, pixelsPerSecond: number): number {
135
+ if (!Number.isFinite(time) || !Number.isFinite(pixelsPerSecond)) return GUTTER;
136
+ return GUTTER + Math.max(0, time) * Math.max(0, pixelsPerSecond);
137
+ }
138
+
139
+ export function getTimelineCanvasHeight(trackCount: number): number {
140
+ return RULER_H + Math.max(0, trackCount) * TRACK_H + TIMELINE_SCROLL_BUFFER;
141
+ }
142
+
143
+ /* ── UI helpers ───────────────────────────────────────────────────── */
144
+ export function shouldShowTimelineShortcutHint(
145
+ scrollHeight: number,
146
+ clientHeight: number,
147
+ ): boolean {
148
+ if (!Number.isFinite(scrollHeight) || !Number.isFinite(clientHeight)) return true;
149
+ return scrollHeight - clientHeight <= 1;
150
+ }
151
+
152
+ export function shouldHandleTimelineDeleteKey(input: {
153
+ key: string;
154
+ metaKey?: boolean;
155
+ ctrlKey?: boolean;
156
+ altKey?: boolean;
157
+ target?: EventTarget | null;
158
+ }): boolean {
159
+ if (input.key !== "Delete" && input.key !== "Backspace") return false;
160
+ if (input.metaKey || input.ctrlKey || input.altKey) return false;
161
+ const target =
162
+ input.target && typeof input.target === "object"
163
+ ? (input.target as {
164
+ tagName?: string;
165
+ isContentEditable?: boolean;
166
+ closest?: (selector: string) => Element | null;
167
+ })
168
+ : null;
169
+ if (target) {
170
+ const tag = target.tagName?.toLowerCase() ?? "";
171
+ if (target.isContentEditable) return false;
172
+ if (["input", "textarea", "select"].includes(tag)) return false;
173
+ if (typeof target.closest === "function" && target.closest("[contenteditable='true']")) {
174
+ return false;
175
+ }
176
+ }
177
+ return true;
178
+ }
179
+
180
+ /* ── Asset drop ───────────────────────────────────────────────────── */
181
+ export function getDefaultDroppedTrack(trackOrder: number[], rowIndex?: number): number {
182
+ if (trackOrder.length === 0) return 0;
183
+ if (rowIndex == null || rowIndex < 0) return trackOrder[0];
184
+ if (rowIndex >= trackOrder.length) {
185
+ return Math.max(...trackOrder) + 1;
186
+ }
187
+ return trackOrder[rowIndex] ?? trackOrder[trackOrder.length - 1] ?? 0;
188
+ }
189
+
190
+ export function resolveTimelineAssetDrop(
191
+ input: {
192
+ rectLeft: number;
193
+ rectTop: number;
194
+ scrollLeft: number;
195
+ scrollTop: number;
196
+ pixelsPerSecond: number;
197
+ duration: number;
198
+ trackHeight: number;
199
+ trackOrder: number[];
200
+ },
201
+ clientX: number,
202
+ clientY: number,
203
+ ): { start: number; track: number } {
204
+ const x = clientX - input.rectLeft + input.scrollLeft - GUTTER;
205
+ const y = clientY - input.rectTop + input.scrollTop - RULER_H;
206
+ const start = Math.max(
207
+ 0,
208
+ Math.min(input.duration, Math.round((x / Math.max(input.pixelsPerSecond, 1)) * 100) / 100),
209
+ );
210
+ const rowIndex = Math.floor(y / Math.max(input.trackHeight, 1));
211
+ return {
212
+ start,
213
+ track: getDefaultDroppedTrack(input.trackOrder, rowIndex),
214
+ };
215
+ }
@@ -0,0 +1,211 @@
1
+ import { formatTime } from "../lib/time";
2
+ import type { ZoomMode } from "../store/playerStore";
3
+
4
+ /* ── Layout constants ─────────────────────────────────────────────── */
5
+ export const GUTTER = 32;
6
+ export const TRACK_H = 72;
7
+ export const RULER_H = 24;
8
+ export const CLIP_Y = 3;
9
+ export const CLIP_HANDLE_W = 18;
10
+ export const TIMELINE_SCROLL_BUFFER = 20;
11
+
12
+ /* ── Tick Generation ────────────────────────────────────────────────── */
13
+ function getMajorTickInterval(duration: number, pixelsPerSecond?: number): number {
14
+ const zoomIntervals = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600];
15
+ if (Number.isFinite(pixelsPerSecond) && (pixelsPerSecond ?? 0) > 0) {
16
+ const targetMajorPx = 128;
17
+ return (
18
+ zoomIntervals.find((interval) => interval * (pixelsPerSecond ?? 0) >= targetMajorPx) ?? 600
19
+ );
20
+ }
21
+ const durationIntervals = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60];
22
+ const target = duration / 6;
23
+ return durationIntervals.find((interval) => interval >= target) ?? 60;
24
+ }
25
+
26
+ function getMinorTickInterval(majorInterval: number, pixelsPerSecond?: number): number {
27
+ let interval = majorInterval / 2;
28
+ if (majorInterval >= 30) interval = majorInterval / 6;
29
+ else if (majorInterval >= 15) interval = majorInterval / 3;
30
+ else if (majorInterval >= 5) interval = majorInterval / 5;
31
+ else if (majorInterval >= 1) interval = majorInterval / 4;
32
+
33
+ if (
34
+ Number.isFinite(pixelsPerSecond) &&
35
+ (pixelsPerSecond ?? 0) > 0 &&
36
+ interval * (pixelsPerSecond ?? 0) < 20
37
+ ) {
38
+ return Math.max(0.25, majorInterval / 2);
39
+ }
40
+ return Math.max(0.25, interval);
41
+ }
42
+
43
+ export function generateTicks(
44
+ duration: number,
45
+ pixelsPerSecond?: number,
46
+ ): { major: number[]; minor: number[] } {
47
+ if (duration <= 0 || !Number.isFinite(duration) || duration > 7200)
48
+ return { major: [], minor: [] };
49
+ const majorInterval = getMajorTickInterval(duration, pixelsPerSecond);
50
+ const minorInterval = getMinorTickInterval(majorInterval, pixelsPerSecond);
51
+ const major: number[] = [];
52
+ const minor: number[] = [];
53
+ const maxTicks = 2000;
54
+ for (
55
+ let t = 0;
56
+ t <= duration + 0.001 && major.length + minor.length < maxTicks;
57
+ t += minorInterval
58
+ ) {
59
+ const rounded = Math.round(t * 100) / 100;
60
+ const isMajor =
61
+ Math.abs(rounded % majorInterval) < 0.01 ||
62
+ Math.abs((rounded % majorInterval) - majorInterval) < 0.01;
63
+ if (isMajor) major.push(rounded);
64
+ else minor.push(rounded);
65
+ }
66
+ return { major, minor };
67
+ }
68
+
69
+ export function formatTimelineTickLabel(time: number, duration: number, majorInterval: number) {
70
+ if (!Number.isFinite(time)) return "0:00";
71
+ const safeTime = Math.max(0, time);
72
+ if (majorInterval < 1) {
73
+ const totalTenths = Math.round(safeTime * 10);
74
+ const wholeSeconds = Math.floor(totalTenths / 10);
75
+ const tenth = totalTenths % 10;
76
+ return `${formatTime(wholeSeconds)}.${tenth}`;
77
+ }
78
+ if (duration >= 3600 || safeTime >= 3600) {
79
+ const totalSeconds = Math.floor(safeTime);
80
+ const hours = Math.floor(totalSeconds / 3600);
81
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
82
+ const seconds = totalSeconds % 60;
83
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
84
+ }
85
+ return formatTime(safeTime);
86
+ }
87
+
88
+ export function shouldAutoScrollTimeline(
89
+ zoomMode: ZoomMode,
90
+ scrollWidth: number,
91
+ clientWidth: number,
92
+ ): boolean {
93
+ if (zoomMode === "fit") return false;
94
+ if (!Number.isFinite(scrollWidth) || !Number.isFinite(clientWidth)) return false;
95
+ return scrollWidth - clientWidth > 1;
96
+ }
97
+
98
+ export function getTimelineScrollLeftForZoomTransition(
99
+ previousZoomMode: ZoomMode | null,
100
+ nextZoomMode: ZoomMode,
101
+ currentScrollLeft: number,
102
+ ): number {
103
+ if (previousZoomMode === "manual" && nextZoomMode === "fit") return 0;
104
+ return currentScrollLeft;
105
+ }
106
+
107
+ export function getTimelineScrollLeftForZoomAnchor(input: {
108
+ pointerX: number;
109
+ currentScrollLeft: number;
110
+ gutter: number;
111
+ currentPixelsPerSecond: number;
112
+ nextPixelsPerSecond: number;
113
+ duration: number;
114
+ }): number {
115
+ const currentPps = Math.max(0, input.currentPixelsPerSecond);
116
+ const nextPps = Math.max(0, input.nextPixelsPerSecond);
117
+ if (
118
+ !Number.isFinite(input.pointerX) ||
119
+ !Number.isFinite(input.currentScrollLeft) ||
120
+ !Number.isFinite(input.duration) ||
121
+ input.duration <= 0 ||
122
+ currentPps <= 0 ||
123
+ nextPps <= 0
124
+ ) {
125
+ return Math.max(0, input.currentScrollLeft);
126
+ }
127
+ const timelineX = Math.max(0, input.currentScrollLeft + input.pointerX - input.gutter);
128
+ const timeAtPointer = Math.max(0, Math.min(input.duration, timelineX / currentPps));
129
+ return Math.max(0, input.gutter + timeAtPointer * nextPps - input.pointerX);
130
+ }
131
+
132
+ export function getTimelinePlayheadLeft(time: number, pixelsPerSecond: number): number {
133
+ if (!Number.isFinite(time) || !Number.isFinite(pixelsPerSecond)) return GUTTER;
134
+ return GUTTER + Math.max(0, time) * Math.max(0, pixelsPerSecond);
135
+ }
136
+
137
+ export function getTimelineCanvasHeight(trackCount: number): number {
138
+ return RULER_H + Math.max(0, trackCount) * TRACK_H + TIMELINE_SCROLL_BUFFER;
139
+ }
140
+
141
+ export function shouldShowTimelineShortcutHint(
142
+ scrollHeight: number,
143
+ clientHeight: number,
144
+ ): boolean {
145
+ if (!Number.isFinite(scrollHeight) || !Number.isFinite(clientHeight)) return true;
146
+ return scrollHeight - clientHeight <= 1;
147
+ }
148
+
149
+ export function shouldHandleTimelineDeleteKey(input: {
150
+ key: string;
151
+ metaKey?: boolean;
152
+ ctrlKey?: boolean;
153
+ altKey?: boolean;
154
+ target?: EventTarget | null;
155
+ }): boolean {
156
+ if (input.key !== "Delete" && input.key !== "Backspace") return false;
157
+ if (input.metaKey || input.ctrlKey || input.altKey) return false;
158
+ const target =
159
+ input.target && typeof input.target === "object"
160
+ ? (input.target as {
161
+ tagName?: string;
162
+ isContentEditable?: boolean;
163
+ closest?: (selector: string) => Element | null;
164
+ })
165
+ : null;
166
+ if (target) {
167
+ const tag = target.tagName?.toLowerCase() ?? "";
168
+ if (target.isContentEditable) return false;
169
+ if (["input", "textarea", "select"].includes(tag)) return false;
170
+ if (typeof target.closest === "function" && target.closest("[contenteditable='true']")) {
171
+ return false;
172
+ }
173
+ }
174
+ return true;
175
+ }
176
+
177
+ export function getDefaultDroppedTrack(trackOrder: number[], rowIndex?: number): number {
178
+ if (trackOrder.length === 0) return 0;
179
+ if (rowIndex == null || rowIndex < 0) return trackOrder[0];
180
+ if (rowIndex >= trackOrder.length) {
181
+ return Math.max(...trackOrder) + 1;
182
+ }
183
+ return trackOrder[rowIndex] ?? trackOrder[trackOrder.length - 1] ?? 0;
184
+ }
185
+
186
+ export function resolveTimelineAssetDrop(
187
+ input: {
188
+ rectLeft: number;
189
+ rectTop: number;
190
+ scrollLeft: number;
191
+ scrollTop: number;
192
+ pixelsPerSecond: number;
193
+ duration: number;
194
+ trackHeight: number;
195
+ trackOrder: number[];
196
+ },
197
+ clientX: number,
198
+ clientY: number,
199
+ ): { start: number; track: number } {
200
+ const x = clientX - input.rectLeft + input.scrollLeft - GUTTER;
201
+ const y = clientY - input.rectTop + input.scrollTop - RULER_H;
202
+ const start = Math.max(
203
+ 0,
204
+ Math.min(input.duration, Math.round((x / Math.max(input.pixelsPerSecond, 1)) * 100) / 100),
205
+ );
206
+ const rowIndex = Math.floor(y / Math.max(input.trackHeight, 1));
207
+ return {
208
+ start,
209
+ track: getDefaultDroppedTrack(input.trackOrder, rowIndex),
210
+ };
211
+ }