@@ -103,6 +103,7 @@ export const NLEPreview = memo(function NLEPreview({
103103 const retiringTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
104104
105105 const zoomRef = useRef < PreviewZoomState > ( loadInitialZoom ( ) ) ;
106+ const [ settledZoom , setSettledZoom ] = useState < PreviewZoomState > ( ( ) => zoomRef . current ) ;
106107 const hudRef = useRef < HTMLDivElement > ( null ) ;
107108 const hudTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
108109 const settleTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
@@ -138,25 +139,28 @@ export const NLEPreview = memo(function NLEPreview({
138139 return ( ) => observer . disconnect ( ) ;
139140 } , [ portrait ] ) ;
140141
142+ const stageSizeRef = useRef ( stageSize ) ;
143+ stageSizeRef . current = stageSize ;
144+
141145 const writeTransform = useCallback ( ( state : PreviewZoomState ) => {
142146 const stage = stageRef . current ;
143147 if ( ! stage ) return ;
144148 const s = toDomPrecision ( state . zoomPercent / 100 ) ;
145149 const px = toDomPrecision ( state . panX ) ;
146150 const py = toDomPrecision ( state . panY ) ;
147- stage . style . transform = `translate (${ px } px, ${ py } px) scale(${ s } )` ;
151+ stage . style . transform = `translate3d (${ px } px, ${ py } px, 0 ) scale(${ s } )` ;
148152 } , [ ] ) ;
149153
150- const applyZoom = useCallback (
151- ( next : PreviewZoomState ) => {
154+ const applyTransform = useCallback (
155+ ( next : PreviewZoomState , showHud : boolean ) => {
152156 const clamped : PreviewZoomState = {
153157 zoomPercent : clampPreviewZoomPercent ( next . zoomPercent ) ,
154158 panX : Number . isFinite ( next . panX ) ? next . panX : 0 ,
155159 panY : Number . isFinite ( next . panY ) ? next . panY : 0 ,
156160 } ;
157161 zoomRef . current = clamped ;
158162
159- if ( ! zoomingRef . current ) {
163+ if ( showHud && ! zoomingRef . current ) {
160164 zoomingRef . current = true ;
161165 const hud = hudRef . current ;
162166 if ( hud ) hud . style . opacity = "1" ;
@@ -169,19 +173,32 @@ export const NLEPreview = memo(function NLEPreview({
169173 zoomingRef . current = false ;
170174 const final = zoomRef . current ;
171175 writeStudioUiPreferences ( { previewZoom : final } ) ;
172- const hud = hudRef . current ;
173- if ( hud ) {
174- hud . textContent = isPreviewAtFit ( final ) ? "Fit" : `${ Math . round ( final . zoomPercent ) } %` ;
175- if ( hudTimerRef . current ) clearTimeout ( hudTimerRef . current ) ;
176- hudTimerRef . current = setTimeout ( ( ) => {
177- if ( hudRef . current ) hudRef . current . style . opacity = "0" ;
178- } , ZOOM_HUD_TIMEOUT_MS ) ;
176+ setSettledZoom ( final ) ;
177+ if ( showHud ) {
178+ const hud = hudRef . current ;
179+ if ( hud ) {
180+ hud . textContent = isPreviewAtFit ( final ) ? "Fit" : `${ Math . round ( final . zoomPercent ) } %` ;
181+ if ( hudTimerRef . current ) clearTimeout ( hudTimerRef . current ) ;
182+ hudTimerRef . current = setTimeout ( ( ) => {
183+ if ( hudRef . current ) hudRef . current . style . opacity = "0" ;
184+ } , ZOOM_HUD_TIMEOUT_MS ) ;
185+ }
179186 }
180187 } , ZOOM_SETTLE_MS ) ;
181188 } ,
182189 [ writeTransform ] ,
183190 ) ;
184191
192+ const applyZoom = useCallback (
193+ ( next : PreviewZoomState ) => applyTransform ( next , true ) ,
194+ [ applyTransform ] ,
195+ ) ;
196+
197+ const applyPan = useCallback (
198+ ( next : PreviewZoomState ) => applyTransform ( next , false ) ,
199+ [ applyTransform ] ,
200+ ) ;
201+
185202 if ( refreshKey !== prevRefreshKeyRef . current ) {
186203 const oldKey = `${ baseKey } :${ prevRefreshKeyRef . current ?? 0 } ` ;
187204 prevRefreshKeyRef . current = refreshKey ;
@@ -228,13 +245,18 @@ export const NLEPreview = memo(function NLEPreview({
228245 event . preventDefault ( ) ;
229246 event . stopPropagation ( ) ;
230247
248+ const sz = stageSizeRef . current ;
249+ const cursorX = event . clientX - ( rect . left + rect . width / 2 ) ;
250+ const cursorY = event . clientY - ( rect . top + rect . height / 2 ) ;
231251 const next = resolvePreviewWheelZoom ( {
232252 state : zoomRef . current ,
233253 deltaY : event . deltaY ,
234254 viewportWidth : rect . width ,
235255 viewportHeight : rect . height ,
236- contentWidth : stageSize . width ,
237- contentHeight : stageSize . height ,
256+ contentWidth : sz . width ,
257+ contentHeight : sz . height ,
258+ cursorX,
259+ cursorY,
238260 } ) ;
239261 applyZoom ( next ) ;
240262 return ;
@@ -245,21 +267,22 @@ export const NLEPreview = memo(function NLEPreview({
245267 event . preventDefault ( ) ;
246268 event . stopPropagation ( ) ;
247269
270+ const sz = stageSizeRef . current ;
248271 const next = resolvePreviewWheelPan ( {
249272 state : zoomRef . current ,
250273 deltaX : event . deltaX ,
251274 deltaY : event . deltaY ,
252275 viewportWidth : rect . width ,
253276 viewportHeight : rect . height ,
254- contentWidth : stageSize . width ,
255- contentHeight : stageSize . height ,
277+ contentWidth : sz . width ,
278+ contentHeight : sz . height ,
256279 } ) ;
257- applyZoom ( next ) ;
280+ applyPan ( next ) ;
258281 } ;
259282
260283 document . addEventListener ( "wheel" , handleWheel , { passive : false , capture : true } ) ;
261284 return ( ) => document . removeEventListener ( "wheel" , handleWheel , { capture : true } ) ;
262- } , [ applyZoom , stageSize . height , stageSize . width ] ) ;
285+ } , [ applyZoom , applyPan ] ) ;
263286
264287 useEffect ( ( ) => {
265288 const viewport = viewportRef . current ;
@@ -320,16 +343,17 @@ export const NLEPreview = memo(function NLEPreview({
320343 if ( ! drag || ! viewport || drag . pointerId !== event . pointerId ) return ;
321344 event . preventDefault ( ) ;
322345 const rect = viewport . getBoundingClientRect ( ) ;
346+ const sz = stageSizeRef . current ;
323347 const pan = clampPreviewPan ( {
324348 panX : drag . originX + event . clientX - drag . startX ,
325349 panY : drag . originY + event . clientY - drag . startY ,
326350 zoomPercent : zoomRef . current . zoomPercent ,
327351 viewportWidth : rect . width ,
328352 viewportHeight : rect . height ,
329- contentWidth : stageSize . width ,
330- contentHeight : stageSize . height ,
353+ contentWidth : sz . width ,
354+ contentHeight : sz . height ,
331355 } ) ;
332- applyZoom ( { ...zoomRef . current , ...pan } ) ;
356+ applyPan ( { ...zoomRef . current , ...pan } ) ;
333357 } ;
334358
335359 const finishDrag = ( event : PointerEvent ) => {
@@ -357,7 +381,7 @@ export const NLEPreview = memo(function NLEPreview({
357381 document . removeEventListener ( "pointercancel" , finishDrag , { capture : true } ) ;
358382 document . removeEventListener ( "auxclick" , handleAuxClick , { capture : true } ) ;
359383 } ;
360- } , [ applyZoom , stageSize . height , stageSize . width ] ) ;
384+ } , [ applyPan ] ) ;
361385
362386 const initial = zoomRef . current ;
363387
@@ -376,7 +400,7 @@ export const NLEPreview = memo(function NLEPreview({
376400 style = { {
377401 width : `${ stageSize . width } px` ,
378402 height : `${ stageSize . height } px` ,
379- transform : `translate (${ toDomPrecision ( initial . panX ) } px, ${ toDomPrecision ( initial . panY ) } px) scale(${ toDomPrecision ( initial . zoomPercent / 100 ) } )` ,
403+ transform : `translate3d (${ toDomPrecision ( initial . panX ) } px, ${ toDomPrecision ( initial . panY ) } px, 0 ) scale(${ toDomPrecision ( initial . zoomPercent / 100 ) } )` ,
380404 transformOrigin : "center center" ,
381405 } }
382406 data-testid = "preview-zoom-stage"
@@ -417,6 +441,17 @@ export const NLEPreview = memo(function NLEPreview({
417441 style = { { opacity : 0 , transition : "opacity 300ms ease-out" } }
418442 aria-live = "polite"
419443 />
444+ { ! isPreviewAtFit ( settledZoom ) && (
445+ < button
446+ type = "button"
447+ className = "absolute bottom-3 right-3 z-50 rounded-md px-2.5 py-1 text-xs font-medium text-white/80 bg-black/50 backdrop-blur-sm hover:bg-black/70 hover:text-white transition-colors"
448+ onClick = { ( ) => applyZoom ( DEFAULT_PREVIEW_ZOOM ) }
449+ aria-label = "Reset zoom to fit"
450+ data-testid = "preview-reset-zoom"
451+ >
452+ { Math . round ( settledZoom . zoomPercent ) } % — Reset
453+ </ button >
454+ ) }
420455 </ div >
421456 </ div >
422457 ) ;
0 commit comments