@@ -10,10 +10,9 @@ import { usePanelLayout } from "./hooks/usePanelLayout";
1010import { useFileManager } from "./hooks/useFileManager" ;
1111import { usePreviewPersistence } from "./hooks/usePreviewPersistence" ;
1212import { useTimelineEditing } from "./hooks/useTimelineEditing" ;
13- import { addBlockToProject } from "./utils/blockInstaller" ;
14- import type { BlockParam } from "@hyperframes/core/registry" ;
1513import type { BlockPreviewInfo } from "./components/sidebar/BlocksTab" ;
1614import { useDomEditSession } from "./hooks/useDomEditSession" ;
15+ import { useBlockHandlers } from "./hooks/useBlockHandlers" ;
1716import { useAppHotkeys } from "./hooks/useAppHotkeys" ;
1817import { useClipboard } from "./hooks/useClipboard" ;
1918import { readStudioUiPreferences , writeStudioUiPreferences } from "./utils/studioUiPreferences" ;
@@ -35,8 +34,7 @@ import type { DomEditSelection } from "./components/editor/domEditing";
3534import { AskAgentModal } from "./components/AskAgentModal" ;
3635import { StudioGlobalDragOverlay } from "./components/StudioGlobalDragOverlay" ;
3736import { StudioHeader } from "./components/StudioHeader" ;
38- import { useGestureRecording } from "./hooks/useGestureRecording" ;
39- import { simplifyGestureSamples } from "./utils/rdpSimplify" ;
37+ import { useGestureCommit } from "./hooks/useGestureCommit" ;
4038
4139import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay" ;
4240import { StudioLeftSidebar } from "./components/StudioLeftSidebar" ;
@@ -82,12 +80,6 @@ export function StudioApp() {
8280 const [ compositionLoading , setCompositionLoading ] = useState ( true ) ;
8381 const [ refreshKey , setRefreshKey ] = useState ( 0 ) ;
8482 const [ , setPreviewDocumentVersion ] = useState ( 0 ) ;
85- const [ activeBlockParams , setActiveBlockParams ] = useState < {
86- blockName : string ;
87- blockTitle : string ;
88- params : BlockParam [ ] ;
89- compositionPath : string ;
90- } | null > ( null ) ;
9183 const [ blockPreview , setBlockPreview ] = useState < BlockPreviewInfo | null > ( null ) ;
9284
9385 const previewIframeRef = useRef < HTMLIFrameElement | null > ( null ) ;
@@ -192,8 +184,15 @@ export function StudioApp() {
192184 isRecordingRef : isGestureRecordingRef ,
193185 } ) ;
194186
195- const blockCtx = useMemo (
196- ( ) => ( {
187+ const {
188+ activeBlockParams,
189+ setActiveBlockParams,
190+ handleAddBlock,
191+ handleTimelineBlockDrop,
192+ handlePreviewBlockDrop,
193+ } = useBlockHandlers ( {
194+ projectId,
195+ blockCtxDeps : {
197196 activeCompPath,
198197 timelineElements,
199198 readProjectFile : fileManager . readProjectFile ,
@@ -202,70 +201,11 @@ export function StudioApp() {
202201 refreshFileTree : fileManager . refreshFileTree ,
203202 reloadPreview,
204203 showToast,
205- } ) ,
206- [
207- activeCompPath ,
208- timelineElements ,
209- fileManager ,
210- editHistory . recordEdit ,
211- reloadPreview ,
212- showToast ,
213- ] ,
214- ) ;
215- const handleAddBlock = useCallback (
216- ( blockName : string ) => {
217- if ( ! projectId ) return ;
218- void ( async ( ) => {
219- const result = await addBlockToProject ( {
220- projectId,
221- blockName,
222- ...blockCtx ,
223- previewIframe : previewIframeRef . current ,
224- currentTime : usePlayerStore . getState ( ) . currentTime ,
225- } ) ;
226- const params = result ?. block . type === "hyperframes:block" ? result . block . params : undefined ;
227- if ( params ?. length ) {
228- setActiveBlockParams ( {
229- blockName : result ! . block . name ,
230- blockTitle : result ! . block . title ,
231- params,
232- compositionPath : result ! . compositionPath ,
233- } ) ;
234- panelLayout . setRightCollapsed ( false ) ;
235- panelLayout . setRightPanelTab ( "block-params" ) ;
236- }
237- } ) ( ) ;
238204 } ,
239- [ projectId , blockCtx , panelLayout ] ,
240- ) ;
241- const handleTimelineBlockDrop = useCallback (
242- ( blockName : string , placement : { start : number ; track : number } ) => {
243- if ( ! projectId ) return ;
244- void addBlockToProject ( {
245- projectId,
246- blockName,
247- placement,
248- ...blockCtx ,
249- previewIframe : previewIframeRef . current ,
250- currentTime : usePlayerStore . getState ( ) . currentTime ,
251- } ) ;
252- } ,
253- [ projectId , blockCtx ] ,
254- ) ;
255- const handlePreviewBlockDrop = useCallback (
256- ( blockName : string , position : { left : number ; top : number } ) => {
257- if ( ! projectId ) return ;
258- void addBlockToProject ( {
259- projectId,
260- blockName,
261- visualPosition : position ,
262- ...blockCtx ,
263- previewIframe : previewIframeRef . current ,
264- currentTime : usePlayerStore . getState ( ) . currentTime ,
265- } ) ;
266- } ,
267- [ projectId , blockCtx ] ,
268- ) ;
205+ previewIframeRef,
206+ setRightCollapsed : panelLayout . setRightCollapsed ,
207+ setRightPanelTab : panelLayout . setRightPanelTab ,
208+ } ) ;
269209
270210 const clearDomSelectionRef = useRef < ( ) => void > ( ( ) => { } ) ;
271211 const domEditSelectionBridgeRef = useRef < DomEditSelection | null > ( null ) ;
@@ -406,125 +346,16 @@ export function StudioApp() {
406346 const dragOverlay = useDragOverlay ( fileManager . handleImportFiles ) ;
407347
408348 // Gesture recording
409- const gestureRecording = useGestureRecording ( ) ;
410- const [ gestureState , setGestureState ] = useState < "idle" | "recording" > ( "idle" ) ;
411- // Synchronous mirror of gestureState — immune to React batching.
412- // Prevents double-R-press within a single render cycle from swallowing the stop.
413- const gestureStateRef = useRef < "idle" | "recording" > ( "idle" ) ;
414- const recordingAutoStopRef = useRef < ReturnType < typeof setInterval > > ( undefined ) ;
415- const recordingStartTimeRef = useRef ( 0 ) ;
416- const commitInFlightRef = useRef ( false ) ;
417349 const handleToggleRecordingRef = useRef < ( ) => void > ( ( ) => { } ) ;
418350 const domEditSessionRef = useRef ( domEditSession ) ;
419351 domEditSessionRef . current = domEditSession ;
420352
421- // Unmount: clear auto-stop interval
422- useEffect ( ( ) => ( ) => clearInterval ( recordingAutoStopRef . current ) , [ ] ) ;
423-
424- // fallow-ignore-next-line complexity
425- const stopAndCommitRecording = useCallback ( async ( ) => {
426- clearInterval ( recordingAutoStopRef . current ) ;
427- if ( commitInFlightRef . current ) return ;
428- commitInFlightRef . current = true ;
429- gestureStateRef . current = "idle" ;
430- isGestureRecordingRef . current = false ;
431- const frozenSamples = gestureRecording . stopRecording ( ) ;
432- const store = usePlayerStore . getState ( ) ;
433- store . setIsPlaying ( false ) ;
434- try {
435- const liveSession = domEditSessionRef . current ;
436- const sel = liveSession . domEditSelection ;
437- if ( ! sel ) {
438- if ( frozenSamples . length > 2 ) {
439- showToast ( "Selection lost during recording" , "error" ) ;
440- }
441- return ;
442- }
443- const duration = frozenSamples . length > 0 ? frozenSamples [ frozenSamples . length - 1 ] ! . time : 0 ;
444-
445- if ( frozenSamples . length <= 2 ) {
446- showToast ( "No gesture detected — move the pointer while recording" , "error" ) ;
447- return ;
448- }
449- if ( duration <= 0 ) {
450- showToast ( "Recording too short — try again" , "error" ) ;
451- return ;
452- }
453-
454- const simplified = simplifyGestureSamples ( frozenSamples , duration , 5 ) ;
455- const sortedPcts = Array . from ( simplified . keys ( ) ) . sort ( ( a , b ) => a - b ) ;
456-
457- // Always create a new tween scoped to the recording range.
458- // Injecting into an existing tween creates keyframes before the recording
459- // start (from the convert-to-keyframes step), causing wrong positions.
460- const selector = sel . id ? `#${ sel . id } ` : sel . selector ;
461- if ( ! selector ) {
462- showToast ( "Cannot save — element has no selector" , "error" ) ;
463- return ;
464- }
465- if ( liveSession . commitMutation ) {
466- const recStart = recordingStartTimeRef . current ;
467- const keyframes = sortedPcts . map ( ( pct ) => ( {
468- percentage : pct ,
469- properties : simplified . get ( pct ) as Record < string , number | string > ,
470- } ) ) ;
471-
472- await liveSession . commitMutation (
473- {
474- type : "add-with-keyframes" ,
475- targetSelector : selector ,
476- position : Math . round ( recStart * 1000 ) / 1000 ,
477- duration : Math . round ( duration * 1000 ) / 1000 ,
478- keyframes,
479- } ,
480- { label : "Gesture recording" , softReload : true } ,
481- ) ;
482- }
483- showToast ( `Recorded ${ sortedPcts . length } keyframes` , "info" ) ;
484- } finally {
485- store . requestSeek ( recordingStartTimeRef . current ) ;
486- gestureRecording . clearSamples ( ) ;
487- setGestureState ( "idle" ) ;
488- commitInFlightRef . current = false ;
489- }
490- } , [ gestureRecording , showToast ] ) ;
491-
492- const handleToggleRecording = useCallback ( ( ) => {
493- if ( gestureStateRef . current === "recording" ) {
494- void stopAndCommitRecording ( ) ;
495- return ;
496- }
497- const sel = domEditSessionRef . current . domEditSelection ;
498- if ( ! sel ) {
499- showToast ( "Select an element first" , "error" ) ;
500- return ;
501- }
502- const iframe = previewIframeRef . current ;
503- if ( ! iframe ) {
504- showToast ( "Preview not ready — try again" , "error" ) ;
505- return ;
506- }
507-
508- const store = usePlayerStore . getState ( ) ;
509- recordingStartTimeRef . current = store . currentTime ;
510- const elStart = Number . parseFloat ( sel . dataAttributes ?. start ?? "0" ) || 0 ;
511- const elDur = Number . parseFloat ( sel . dataAttributes ?. duration ?? "0" ) || 0 ;
512- const elementEnd = elDur > 0 ? elStart + elDur : undefined ;
513- gestureRecording . startRecording ( sel . element , iframe , elementEnd ) ;
514- gestureStateRef . current = "recording" ;
515- isGestureRecordingRef . current = true ;
516- setGestureState ( "recording" ) ;
517-
518- clearInterval ( recordingAutoStopRef . current ) ;
519- const autoStopAt = elementEnd ?? Infinity ;
520- recordingAutoStopRef . current = setInterval ( ( ) => {
521- const { currentTime : t , duration : d } = usePlayerStore . getState ( ) ;
522- const limit = Math . min ( autoStopAt , d ) ;
523- if ( limit > 0 && t >= limit - 0.05 ) {
524- void stopAndCommitRecording ( ) ;
525- }
526- } , 100 ) ;
527- } , [ gestureRecording , showToast , stopAndCommitRecording ] ) ;
353+ const { gestureState, gestureRecording, handleToggleRecording } = useGestureCommit ( {
354+ domEditSessionRef,
355+ previewIframeRef,
356+ showToast,
357+ isGestureRecordingRef,
358+ } ) ;
528359 handleToggleRecordingRef . current = handleToggleRecording ;
529360
530361 const handlePreviewIframeRef = useCallback (
0 commit comments