@hyperframes/studio 0.6.27 → 0.6.28

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.
package/dist/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>HyperFrames Studio</title>
8
- <script type="module" crossorigin src="/assets/index-DYjmgXgg.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-EdfhuQ5T.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-DVpLGNHi.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.6.27",
3
+ "version": "0.6.28",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,8 +30,9 @@
30
30
  "@codemirror/theme-one-dark": "^6.1.2",
31
31
  "@codemirror/view": "6.40.0",
32
32
  "@phosphor-icons/react": "^2.1.10",
33
- "@hyperframes/core": "0.6.27",
34
- "@hyperframes/player": "0.6.27"
33
+ "mediabunny": "^1.45.3",
34
+ "@hyperframes/core": "0.6.28",
35
+ "@hyperframes/player": "0.6.28"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@types/react": "19",
@@ -45,7 +46,7 @@
45
46
  "vite": "^6.4.2",
46
47
  "vitest": "^3.2.4",
47
48
  "zustand": "^5.0.0",
48
- "@hyperframes/producer": "0.6.27"
49
+ "@hyperframes/producer": "0.6.28"
49
50
  },
50
51
  "peerDependencies": {
51
52
  "react": "19",
package/src/App.tsx CHANGED
@@ -8,7 +8,7 @@ import { useCaptionSync } from "./captions/hooks/useCaptionSync";
8
8
  import { usePersistentEditHistory } from "./hooks/usePersistentEditHistory";
9
9
  import { usePanelLayout } from "./hooks/usePanelLayout";
10
10
  import { useFileManager } from "./hooks/useFileManager";
11
- import { useManifestPersistence } from "./hooks/useManifestPersistence";
11
+ import { usePreviewPersistence } from "./hooks/usePreviewPersistence";
12
12
  import { useTimelineEditing } from "./hooks/useTimelineEditing";
13
13
  import { addBlockToProject } from "./utils/blockInstaller";
14
14
  import type { BlockParam } from "@hyperframes/core/registry";
@@ -117,12 +117,9 @@ export function StudioApp() {
117
117
  });
118
118
  const editHistory = usePersistentEditHistory({ projectId });
119
119
  const domEditSaveTimestampRef = useRef(0);
120
+ const pendingTimelineEditPathRef = useRef(new Set<string>());
120
121
  const reloadPreview = useCallback(() => {
121
- try {
122
- previewIframeRef.current?.contentWindow?.location.reload();
123
- } catch {
124
- setRefreshKey((k) => k + 1);
125
- }
122
+ setRefreshKey((k) => k + 1);
126
123
  }, []);
127
124
 
128
125
  const fileManager = useFileManager({
@@ -145,7 +142,7 @@ export function StudioApp() {
145
142
  setActiveCompPathHydrated(true);
146
143
  }, [activeCompPathHydrated, fileManager.fileTree, fileManager.fileTreeLoaded]);
147
144
 
148
- const manifestPersistence = useManifestPersistence({
145
+ const previewPersistence = usePreviewPersistence({
149
146
  projectId,
150
147
  showToast,
151
148
  readOptionalProjectFile: fileManager.readOptionalProjectFile,
@@ -155,6 +152,7 @@ export function StudioApp() {
155
152
  activeCompPathRef,
156
153
  domEditSaveTimestampRef,
157
154
  reloadPreview: () => setRefreshKey((k) => k + 1),
155
+ pendingTimelineEditPathRef,
158
156
  });
159
157
 
160
158
  const timelineEditing = useTimelineEditing({
@@ -166,6 +164,8 @@ export function StudioApp() {
166
164
  recordEdit: editHistory.recordEdit,
167
165
  domEditSaveTimestampRef,
168
166
  reloadPreview,
167
+ previewIframeRef,
168
+ pendingTimelineEditPathRef,
169
169
  uploadProjectFiles: fileManager.uploadProjectFiles,
170
170
  });
171
171
 
@@ -274,8 +274,8 @@ export function StudioApp() {
274
274
  writeProjectFile: fileManager.writeProjectFile,
275
275
  domEditSaveTimestampRef,
276
276
  showToast,
277
- syncHistoryPreviewAfterApply: manifestPersistence.syncHistoryPreviewAfterApply,
278
- waitForPendingDomEditSaves: manifestPersistence.waitForPendingDomEditSaves,
277
+ syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
278
+ waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
279
279
  leftSidebarRef,
280
280
  handleCopy,
281
281
  handlePaste,
@@ -297,7 +297,7 @@ export function StudioApp() {
297
297
  setRightPanelTab: panelLayout.setRightPanelTab,
298
298
  showToast,
299
299
  refreshPreviewDocumentVersion,
300
- queueDomEditSave: manifestPersistence.queueDomEditSave,
300
+ queueDomEditSave: previewPersistence.queueDomEditSave,
301
301
  readProjectFile: fileManager.readProjectFile,
302
302
  writeProjectFile: fileManager.writeProjectFile,
303
303
  domEditSaveTimestampRef,
@@ -309,7 +309,7 @@ export function StudioApp() {
309
309
  previewIframe,
310
310
  refreshKey,
311
311
  rightPanelTab: panelLayout.rightPanelTab,
312
- applyStudioManualEditsToPreviewRef: manifestPersistence.applyStudioManualEditsToPreviewRef,
312
+ applyStudioManualEditsToPreviewRef: previewPersistence.applyStudioManualEditsToPreviewRef,
313
313
  syncPreviewHistoryHotkey: appHotkeys.syncPreviewHistoryHotkey,
314
314
  reloadPreview,
315
315
  setRefreshKey,
@@ -345,7 +345,7 @@ export function StudioApp() {
345
345
  projectId,
346
346
  activeCompPath,
347
347
  showToast,
348
- waitForPendingDomEditSaves: manifestPersistence.waitForPendingDomEditSaves,
348
+ waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
349
349
  });
350
350
  const {
351
351
  consoleErrors,
@@ -453,7 +453,7 @@ export function StudioApp() {
453
453
  startRender: renderQueue.startRender as (options: unknown) => Promise<void>,
454
454
  },
455
455
  compositionDimensions,
456
- waitForPendingDomEditSaves: manifestPersistence.waitForPendingDomEditSaves,
456
+ waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
457
457
  handlePreviewIframeRef,
458
458
  refreshPreviewDocumentVersion,
459
459
  timelineVisible,
@@ -17,7 +17,7 @@ interface RecordEditInput {
17
17
  files: Record<string, { before: string; after: string }>;
18
18
  }
19
19
 
20
- interface UseManifestPersistenceParams {
20
+ interface UsePreviewPersistenceParams {
21
21
  projectId: string | null;
22
22
  showToast: (message: string, tone?: "error" | "info") => void;
23
23
  readOptionalProjectFile: (path: string) => Promise<string>;
@@ -26,15 +26,18 @@ interface UseManifestPersistenceParams {
26
26
  previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
27
27
  activeCompPathRef: React.MutableRefObject<string | null>;
28
28
  /** Shared timestamp ref — written by any studio save (code tab, timeline, DOM edits).
29
- * Used to suppress SSE echoes so we don't double-reload after our own saves. */
29
+ * Used to suppress file-change echoes so we don't reload after our own saves. */
30
30
  domEditSaveTimestampRef: React.MutableRefObject<number>;
31
+ /** Tracks in-flight timeline edits that patch the iframe DOM directly. File-change
32
+ * events for these paths are always suppressed since the preview is already up-to-date. */
33
+ pendingTimelineEditPathRef?: React.MutableRefObject<Set<string>>;
31
34
  /** Called to reload the preview after undo/redo or external file changes. */
32
35
  reloadPreview: () => void;
33
36
  }
34
37
 
35
38
  // ── Hook ──
36
39
 
37
- export function useManifestPersistence({
40
+ export function usePreviewPersistence({
38
41
  projectId,
39
42
  showToast: _showToast,
40
43
  readOptionalProjectFile: _readOptionalProjectFile,
@@ -44,7 +47,8 @@ export function useManifestPersistence({
44
47
  activeCompPathRef: _activeCompPathRef,
45
48
  domEditSaveTimestampRef,
46
49
  reloadPreview,
47
- }: UseManifestPersistenceParams) {
50
+ pendingTimelineEditPathRef,
51
+ }: UsePreviewPersistenceParams) {
48
52
  void _showToast;
49
53
  void _recordEdit;
50
54
  void _activeCompPathRef;
@@ -162,8 +166,11 @@ export function useManifestPersistence({
162
166
  const handler = (payload?: unknown) => {
163
167
  const changedPath = readStudioFileChangePath(payload);
164
168
  if (!changedPath) return;
165
- const recentDomEditSave = Date.now() - domEditSaveTimestampRef.current < 1200;
166
- // External file change — reload unless it's an echo of our own save.
169
+ const recentDomEditSave = Date.now() - domEditSaveTimestampRef.current < 4000;
170
+ if (pendingTimelineEditPathRef?.current.has(changedPath)) {
171
+ pendingTimelineEditPathRef.current.delete(changedPath);
172
+ return;
173
+ }
167
174
  if (!recentDomEditSave) {
168
175
  reloadPreview();
169
176
  }
@@ -38,6 +38,8 @@ interface UseTimelineEditingOptions {
38
38
  recordEdit: (input: RecordEditInput) => Promise<void>;
39
39
  domEditSaveTimestampRef: React.MutableRefObject<number>;
40
40
  reloadPreview: () => void;
41
+ previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
42
+ pendingTimelineEditPathRef: React.MutableRefObject<Set<string>>;
41
43
  uploadProjectFiles: (files: Iterable<File>, dir?: string) => Promise<string[]>;
42
44
  }
43
45
 
@@ -53,6 +55,97 @@ function buildPatchTarget(element: { domId?: string; selector?: string; selector
53
55
  return null;
54
56
  }
55
57
 
58
+ // The runtime re-reads data-start/data-duration from the DOM on each sync tick
59
+ // (packages/core/src/runtime/init.ts:1324-1368), so attribute mutations here are
60
+ // picked up automatically on the next frame without a rebind call.
61
+ function patchIframeDomTiming(
62
+ iframe: HTMLIFrameElement | null,
63
+ element: TimelineElement,
64
+ attrs: Array<[string, string]>,
65
+ ): void {
66
+ try {
67
+ const doc = iframe?.contentDocument;
68
+ if (!doc) return;
69
+ const el = element.domId
70
+ ? doc.getElementById(element.domId)
71
+ : element.selector
72
+ ? (doc.querySelectorAll(element.selector)[element.selectorIndex ?? 0] ?? null)
73
+ : null;
74
+ if (!el) return;
75
+ for (const [name, value] of attrs) el.setAttribute(name, value);
76
+ } catch {
77
+ // Cross-origin or mid-navigation — file save is enqueued; iframe patch is best-effort.
78
+ }
79
+ }
80
+
81
+ function resolveResizePlaybackStart(
82
+ original: string,
83
+ target: PatchTarget,
84
+ element: TimelineElement,
85
+ updates: Pick<TimelineElement, "start" | "playbackStart">,
86
+ ): { attrName: string; value: number } | null {
87
+ if (updates.playbackStart != null) {
88
+ const attrName =
89
+ element.playbackStartAttr === "playback-start" ? "playback-start" : "media-start";
90
+ return { attrName, value: updates.playbackStart };
91
+ }
92
+ const trimDelta = updates.start - element.start;
93
+ if (trimDelta === 0) return null;
94
+ const raw =
95
+ readAttributeByTarget(original, target, "playback-start") ??
96
+ readAttributeByTarget(original, target, "media-start");
97
+ const current = raw != null ? parseFloat(raw) : undefined;
98
+ if (current == null || !Number.isFinite(current)) return null;
99
+ const attrName =
100
+ element.playbackStartAttr === "playback-start" ? "playback-start" : "media-start";
101
+ return {
102
+ attrName,
103
+ value: Math.max(0, current + trimDelta * Math.max(element.playbackRate ?? 1, 0.1)),
104
+ };
105
+ }
106
+
107
+ type PatchTarget = NonNullable<ReturnType<typeof buildPatchTarget>>;
108
+
109
+ interface PersistTimelineEditInput {
110
+ projectId: string;
111
+ element: TimelineElement;
112
+ activeCompPath: string | null;
113
+ label: string;
114
+ buildPatches: (original: string, target: PatchTarget) => string;
115
+ writeProjectFile: (path: string, content: string) => Promise<void>;
116
+ recordEdit: (input: RecordEditInput) => Promise<void>;
117
+ domEditSaveTimestampRef: React.MutableRefObject<number>;
118
+ pendingTimelineEditPathRef: React.MutableRefObject<Set<string>>;
119
+ }
120
+
121
+ async function persistTimelineEdit(input: PersistTimelineEditInput): Promise<void> {
122
+ const targetPath = input.element.sourceFile || input.activeCompPath || "index.html";
123
+ const originalContent = await readFileContent(input.projectId, targetPath);
124
+
125
+ const patchTarget = buildPatchTarget(input.element);
126
+ if (!patchTarget) {
127
+ throw new Error(`Timeline element ${input.element.id} is missing a patchable target`);
128
+ }
129
+
130
+ const patchedContent = input.buildPatches(originalContent, patchTarget);
131
+ if (patchedContent === originalContent) {
132
+ throw new Error(`Unable to patch timeline element ${input.element.id} in ${targetPath}`);
133
+ }
134
+
135
+ input.pendingTimelineEditPathRef.current.add(targetPath);
136
+ input.domEditSaveTimestampRef.current = Date.now();
137
+ await saveProjectFilesWithHistory({
138
+ projectId: input.projectId,
139
+ label: input.label,
140
+ kind: "timeline",
141
+ files: { [targetPath]: patchedContent },
142
+ readFile: async () => originalContent,
143
+ writeFile: input.writeProjectFile,
144
+ recordEdit: input.recordEdit,
145
+ });
146
+ input.domEditSaveTimestampRef.current = Date.now();
147
+ }
148
+
56
149
  async function readFileContent(projectId: string, targetPath: string): Promise<string> {
57
150
  const response = await fetch(
58
151
  `/api/projects/${projectId}/files/${encodeURIComponent(targetPath)}`,
@@ -78,127 +171,105 @@ export function useTimelineEditing({
78
171
  recordEdit,
79
172
  domEditSaveTimestampRef,
80
173
  reloadPreview,
174
+ previewIframeRef,
175
+ pendingTimelineEditPathRef,
81
176
  uploadProjectFiles,
82
177
  }: UseTimelineEditingOptions) {
83
178
  const projectIdRef = useRef(projectId);
84
179
  projectIdRef.current = projectId;
85
180
 
181
+ const editQueueRef = useRef(Promise.resolve());
86
182
  const lastBlockedTimelineToastAtRef = useRef(0);
87
183
 
88
- const handleTimelineElementMove = useCallback(
89
- async (element: TimelineElement, updates: Pick<TimelineElement, "start" | "track">) => {
184
+ const enqueueEdit = useCallback(
185
+ (
186
+ element: TimelineElement,
187
+ label: string,
188
+ buildPatches: PersistTimelineEditInput["buildPatches"],
189
+ ): Promise<void> => {
90
190
  const pid = projectIdRef.current;
91
- if (!pid) throw new Error("No active project");
92
-
93
- const targetPath = element.sourceFile || activeCompPath || "index.html";
94
- const originalContent = await readFileContent(pid, targetPath);
95
-
96
- const patchTarget = buildPatchTarget(element);
97
- if (!patchTarget) {
98
- throw new Error(`Timeline element ${element.id} is missing a patchable target`);
99
- }
100
-
101
- let patchedContent = applyPatchByTarget(originalContent, patchTarget, {
102
- type: "attribute",
103
- property: "start",
104
- value: formatTimelineAttributeNumber(updates.start),
105
- });
106
- patchedContent = applyPatchByTarget(patchedContent, patchTarget, {
107
- type: "attribute",
108
- property: "track-index",
109
- value: String(updates.track),
191
+ if (!pid) return Promise.resolve();
192
+ const queued = editQueueRef.current.then(() =>
193
+ persistTimelineEdit({
194
+ projectId: pid,
195
+ element,
196
+ activeCompPath,
197
+ label,
198
+ buildPatches,
199
+ writeProjectFile,
200
+ recordEdit,
201
+ domEditSaveTimestampRef,
202
+ pendingTimelineEditPathRef,
203
+ }),
204
+ );
205
+ editQueueRef.current = queued.catch((error) => {
206
+ console.error(`[Timeline] Failed to persist: ${label}`, error);
110
207
  });
208
+ return queued;
209
+ },
210
+ [
211
+ activeCompPath,
212
+ recordEdit,
213
+ writeProjectFile,
214
+ domEditSaveTimestampRef,
215
+ pendingTimelineEditPathRef,
216
+ ],
217
+ );
111
218
 
112
- if (patchedContent === originalContent) {
113
- throw new Error(`Unable to patch timeline element ${element.id} in ${targetPath}`);
114
- }
115
-
116
- domEditSaveTimestampRef.current = Date.now();
117
- await saveProjectFilesWithHistory({
118
- projectId: pid,
119
- label: "Move timeline clip",
120
- kind: "timeline",
121
- files: { [targetPath]: patchedContent },
122
- readFile: async () => originalContent,
123
- writeFile: writeProjectFile,
124
- recordEdit,
219
+ const handleTimelineElementMove = useCallback(
220
+ (element: TimelineElement, updates: Pick<TimelineElement, "start" | "track">) => {
221
+ patchIframeDomTiming(previewIframeRef.current, element, [
222
+ ["data-start", formatTimelineAttributeNumber(updates.start)],
223
+ ["data-track-index", String(updates.track)],
224
+ ]);
225
+ return enqueueEdit(element, "Move timeline clip", (original, target) => {
226
+ let patched = applyPatchByTarget(original, target, {
227
+ type: "attribute",
228
+ property: "start",
229
+ value: formatTimelineAttributeNumber(updates.start),
230
+ });
231
+ return applyPatchByTarget(patched, target, {
232
+ type: "attribute",
233
+ property: "track-index",
234
+ value: String(updates.track),
235
+ });
125
236
  });
126
-
127
- reloadPreview();
128
237
  },
129
- [activeCompPath, recordEdit, writeProjectFile, domEditSaveTimestampRef, reloadPreview],
238
+ [previewIframeRef, enqueueEdit],
130
239
  );
131
240
 
132
241
  const handleTimelineElementResize = useCallback(
133
- async (
242
+ (
134
243
  element: TimelineElement,
135
244
  updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
136
245
  ) => {
137
- const pid = projectIdRef.current;
138
- if (!pid) throw new Error("No active project");
139
-
140
- const targetPath = element.sourceFile || activeCompPath || "index.html";
141
- const originalContent = await readFileContent(pid, targetPath);
142
-
143
- const patchTarget = buildPatchTarget(element);
144
- if (!patchTarget) {
145
- throw new Error(`Timeline element ${element.id} is missing a patchable target`);
146
- }
147
-
148
- const playbackStartAttrName =
149
- element.playbackStartAttr === "playback-start" ? "playback-start" : "media-start";
150
- const currentPlaybackStartValue =
151
- readAttributeByTarget(originalContent, patchTarget, "playback-start") ??
152
- readAttributeByTarget(originalContent, patchTarget, "media-start");
153
- const currentPlaybackStart =
154
- currentPlaybackStartValue != null ? parseFloat(currentPlaybackStartValue) : undefined;
155
- const trimDelta = updates.start - element.start;
156
- const fallbackPlaybackStart =
157
- updates.playbackStart == null &&
158
- trimDelta !== 0 &&
159
- Number.isFinite(currentPlaybackStart) &&
160
- currentPlaybackStart != null
161
- ? Math.max(0, currentPlaybackStart + trimDelta * Math.max(element.playbackRate ?? 1, 0.1))
162
- : undefined;
163
- const nextPlaybackStart = updates.playbackStart ?? fallbackPlaybackStart;
164
-
165
- let patchedContent = originalContent;
166
- patchedContent = applyPatchByTarget(patchedContent, patchTarget, {
167
- type: "attribute",
168
- property: "start",
169
- value: formatTimelineAttributeNumber(updates.start),
170
- });
171
- patchedContent = applyPatchByTarget(patchedContent, patchTarget, {
172
- type: "attribute",
173
- property: "duration",
174
- value: formatTimelineAttributeNumber(updates.duration),
175
- });
176
- if (nextPlaybackStart != null) {
177
- patchedContent = applyPatchByTarget(patchedContent, patchTarget, {
246
+ patchIframeDomTiming(previewIframeRef.current, element, [
247
+ ["data-start", formatTimelineAttributeNumber(updates.start)],
248
+ ["data-duration", formatTimelineAttributeNumber(updates.duration)],
249
+ ]);
250
+ return enqueueEdit(element, "Resize timeline clip", (original, target) => {
251
+ const pbs = resolveResizePlaybackStart(original, target, element, updates);
252
+ let patched = applyPatchByTarget(original, target, {
178
253
  type: "attribute",
179
- property: playbackStartAttrName,
180
- value: formatTimelineAttributeNumber(nextPlaybackStart),
254
+ property: "start",
255
+ value: formatTimelineAttributeNumber(updates.start),
181
256
  });
182
- }
183
-
184
- if (patchedContent === originalContent) {
185
- throw new Error(`Unable to patch timeline element ${element.id} in ${targetPath}`);
186
- }
187
-
188
- domEditSaveTimestampRef.current = Date.now();
189
- await saveProjectFilesWithHistory({
190
- projectId: pid,
191
- label: "Resize timeline clip",
192
- kind: "timeline",
193
- files: { [targetPath]: patchedContent },
194
- readFile: async () => originalContent,
195
- writeFile: writeProjectFile,
196
- recordEdit,
257
+ patched = applyPatchByTarget(patched, target, {
258
+ type: "attribute",
259
+ property: "duration",
260
+ value: formatTimelineAttributeNumber(updates.duration),
261
+ });
262
+ if (pbs) {
263
+ patched = applyPatchByTarget(patched, target, {
264
+ type: "attribute",
265
+ property: pbs.attrName,
266
+ value: formatTimelineAttributeNumber(pbs.value),
267
+ });
268
+ }
269
+ return patched;
197
270
  });
198
-
199
- reloadPreview();
200
271
  },
201
- [activeCompPath, recordEdit, writeProjectFile, domEditSaveTimestampRef, reloadPreview],
272
+ [previewIframeRef, enqueueEdit],
202
273
  );
203
274
 
204
275
  const handleTimelineElementDelete = useCallback(
@@ -252,6 +252,7 @@ export const TimelineCanvas = memo(function TimelineCanvas({
252
252
  isHovered={hoveredClip === clipKey}
253
253
  isDragging={false}
254
254
  hasCustomContent={!!renderClipContent}
255
+ capabilities={capabilities}
255
256
  theme={theme}
256
257
  trackStyle={clipStyle}
257
258
  isComposition={isComposition}
@@ -369,6 +370,7 @@ export const TimelineCanvas = memo(function TimelineCanvas({
369
370
  isHovered={false}
370
371
  isDragging={true}
371
372
  hasCustomContent={!!renderClipContent}
373
+ capabilities={getTimelineEditCapabilities(activeDraggedElement)}
372
374
  theme={theme}
373
375
  trackStyle={getTrackStyle(activeDraggedElement.tag)}
374
376
  isComposition={!!activeDraggedElement.compositionSrc}
@@ -3,7 +3,7 @@ import type { TimelineTrackStyle } from "./timelineTheme";
3
3
  import { memo, type ReactNode } from "react";
4
4
  import type { TimelineElement } from "../store/playerStore";
5
5
  import { defaultTimelineTheme, getClipHandleOpacity, type TimelineTheme } from "./timelineTheme";
6
- import { getTimelineEditCapabilities } from "./timelineEditing";
6
+ import type { TimelineEditCapabilities } from "./timelineEditing";
7
7
 
8
8
  interface TimelineClipProps {
9
9
  el: TimelineElement;
@@ -13,6 +13,7 @@ interface TimelineClipProps {
13
13
  isHovered: boolean;
14
14
  isDragging?: boolean;
15
15
  hasCustomContent: boolean;
16
+ capabilities: TimelineEditCapabilities;
16
17
  theme?: TimelineTheme;
17
18
  trackStyle: TimelineTrackStyle;
18
19
  isComposition: boolean;
@@ -33,6 +34,7 @@ export const TimelineClip = memo(function TimelineClip({
33
34
  isHovered,
34
35
  isDragging = false,
35
36
  hasCustomContent,
37
+ capabilities,
36
38
  theme = defaultTimelineTheme,
37
39
  trackStyle,
38
40
  isComposition,
@@ -47,6 +49,7 @@ export const TimelineClip = memo(function TimelineClip({
47
49
  const leftPx = el.start * pps;
48
50
  const widthPx = Math.max(el.duration * pps, 4);
49
51
  const handleOpacity = getClipHandleOpacity({ isHovered, isSelected, isDragging });
52
+
50
53
  const borderColor = isSelected
51
54
  ? theme.clipBorderActive
52
55
  : isHovered
@@ -59,7 +62,6 @@ export const TimelineClip = memo(function TimelineClip({
59
62
  : isHovered
60
63
  ? theme.clipShadowHover
61
64
  : theme.clipShadow;
62
- const capabilities = getTimelineEditCapabilities(el);
63
65
  const displayLabel = el.label || el.id || el.tag;
64
66
  const showHandles = handleOpacity > 0.01;
65
67