@@ -29,6 +29,8 @@ import {
2929 readFileContent ,
3030 applyPatchByTarget ,
3131 formatTimelineAttributeNumber ,
32+ shiftGsapPositions ,
33+ scaleGsapPositions ,
3234} from "./timelineEditingHelpers" ;
3335import type { PersistTimelineEditInput } from "./timelineEditingHelpers" ;
3436import { sdkTimingPersist } from "../utils/sdkCutover" ;
@@ -91,6 +93,7 @@ export function useTimelineEditing({
9193 element : TimelineElement ,
9294 label : string ,
9395 buildPatches : PersistTimelineEditInput [ "buildPatches" ] ,
96+ coalesceKey ?: string ,
9497 ) : Promise < void > => {
9598 if ( isRecordingRef ?. current ) {
9699 showToast ( "Cannot edit timeline while recording" , "error" ) ;
@@ -110,6 +113,7 @@ export function useTimelineEditing({
110113 recordEdit,
111114 domEditSaveTimestampRef,
112115 pendingTimelineEditPathRef,
116+ coalesceKey,
113117 } ) ,
114118 )
115119 . then ( ( ) => {
@@ -154,6 +158,24 @@ export function useTimelineEditing({
154158 value : String ( updates . track ) ,
155159 } ) ;
156160 } ;
161+ // Server-path fallback (no SDK session): persist the attr patch, then
162+ // shift GSAP tween positions on the server and reload the preview — the
163+ // SDK path folds both into setTiming, but the fallback must do them
164+ // explicitly or the clip moves while its GSAP tweens stay put + the
165+ // preview never refreshes. coalesceKey mirrors the SDK branch so undo
166+ // granularity is identical on either path.
167+ const coalesceKey = `timeline-move:${ element . hfId ?? element . id } ` ;
168+ const moveFallback = ( ) =>
169+ enqueueEdit ( element , "Move timeline clip" , buildMovePatches , coalesceKey ) . then ( ( ) => {
170+ const pid = projectIdRef . current ;
171+ const delta = updates . start - element . start ;
172+ if ( delta !== 0 && element . domId && pid ) {
173+ return shiftGsapPositions ( pid , targetPath , element . domId , delta )
174+ . then ( ( ) => reloadPreview ( ) )
175+ . catch ( ( err ) => console . error ( "[Timeline] Failed to shift GSAP positions" , err ) ) ;
176+ }
177+ return reloadPreview ( ) ;
178+ } ) ;
157179 if ( sdkSession && element . hfId ) {
158180 return sdkTimingPersist (
159181 element . hfId ,
@@ -167,12 +189,12 @@ export function useTimelineEditing({
167189 domEditSaveTimestampRef,
168190 compositionPath : activeCompPath ,
169191 } ,
170- { label : "Move timeline clip" , coalesceKey : `timeline-move: ${ element . hfId } ` } ,
192+ { label : "Move timeline clip" , coalesceKey } ,
171193 ) . then ( ( handled ) => {
172- if ( ! handled ) return enqueueEdit ( element , "Move timeline clip" , buildMovePatches ) ;
194+ if ( ! handled ) return moveFallback ( ) ;
173195 } ) ;
174196 }
175- return enqueueEdit ( element , "Move timeline clip" , buildMovePatches ) ;
197+ return moveFallback ( ) ;
176198 } ,
177199 [
178200 previewIframeRef ,
@@ -193,10 +215,21 @@ export function useTimelineEditing({
193215 element : TimelineElement ,
194216 updates : Pick < TimelineElement , "start" | "duration" | "playbackStart" > ,
195217 ) => {
196- patchIframeDomTiming ( previewIframeRef . current , element , [
218+ const liveAttrs : Array < [ string , string ] > = [
197219 [ "data-start" , formatTimelineAttributeNumber ( updates . start ) ] ,
198220 [ "data-duration" , formatTimelineAttributeNumber ( updates . duration ) ] ,
199- ] ) ;
221+ ] ;
222+ // Patch the live playback-start/media-start attr too, or a resize that
223+ // trims the playback start leaves the preview showing the old in-point
224+ // until the next reload (the persisted patch handles it via pbs below).
225+ if ( updates . playbackStart != null ) {
226+ const liveAttr =
227+ element . playbackStartAttr === "playback-start"
228+ ? "data-playback-start"
229+ : "data-media-start" ;
230+ liveAttrs . push ( [ liveAttr , formatTimelineAttributeNumber ( updates . playbackStart ) ] ) ;
231+ }
232+ patchIframeDomTiming ( previewIframeRef . current , element , liveAttrs ) ;
200233 const targetPath = element . sourceFile || activeCompPath || "index.html" ;
201234 const buildResizePatches : PersistTimelineEditInput [ "buildPatches" ] = ( original , target ) => {
202235 const pbs = resolveResizePlaybackStart ( original , target , element , updates ) ;
@@ -220,10 +253,38 @@ export function useTimelineEditing({
220253 return patched ;
221254 } ;
222255 // SDK path: skip when a playback-start adjustment is needed (setTiming has no pbs field).
223- // Condition: no explicit pbs override AND (no start change OR element has no pbs attribute).
256+ // The second clause fires because trimming the start of a clip that has a
257+ // playback-start attribute implicitly shifts that in-point — which the SDK
258+ // setTiming op can't express — so those resizes must take the server path.
224259 const hasPbsAdjustment =
225260 updates . playbackStart != null ||
226261 ( updates . start !== element . start && element . playbackStart != null ) ;
262+ // Server-path fallback: after persisting the attr patch, scale GSAP tween
263+ // positions/durations on the server and reload the preview. The SDK path
264+ // folds both into setTiming; the fallback must do them explicitly or the
265+ // clip resizes while its GSAP tweens keep their old timing + the preview
266+ // never refreshes. coalesceKey mirrors the SDK branch for undo parity.
267+ const coalesceKey = `timeline-resize:${ element . hfId ?? element . id } ` ;
268+ const timingChanged =
269+ updates . start !== element . start || updates . duration !== element . duration ;
270+ const resizeFallback = ( ) =>
271+ enqueueEdit ( element , "Resize timeline clip" , buildResizePatches , coalesceKey ) . then ( ( ) => {
272+ const pid = projectIdRef . current ;
273+ if ( timingChanged && element . domId && pid ) {
274+ return scaleGsapPositions (
275+ pid ,
276+ targetPath ,
277+ element . domId ,
278+ element . start ,
279+ element . duration ,
280+ updates . start ,
281+ updates . duration ,
282+ )
283+ . then ( ( ) => reloadPreview ( ) )
284+ . catch ( ( err ) => console . error ( "[Timeline] Failed to scale GSAP positions" , err ) ) ;
285+ }
286+ return reloadPreview ( ) ;
287+ } ) ;
227288 if ( sdkSession && element . hfId && ! hasPbsAdjustment ) {
228289 return sdkTimingPersist (
229290 element . hfId ,
@@ -237,12 +298,12 @@ export function useTimelineEditing({
237298 domEditSaveTimestampRef,
238299 compositionPath : activeCompPath ,
239300 } ,
240- { label : "Resize timeline clip" , coalesceKey : `timeline-resize: ${ element . hfId } ` } ,
301+ { label : "Resize timeline clip" , coalesceKey } ,
241302 ) . then ( ( handled ) => {
242- if ( ! handled ) return enqueueEdit ( element , "Resize timeline clip" , buildResizePatches ) ;
303+ if ( ! handled ) return resizeFallback ( ) ;
243304 } ) ;
244305 }
245- return enqueueEdit ( element , "Resize timeline clip" , buildResizePatches ) ;
306+ return resizeFallback ( ) ;
246307 } ,
247308 [
248309 previewIframeRef ,
0 commit comments