11import { memo , useMemo , useRef , useState , type RefObject } from "react" ;
22import { useMountEffect } from "../../hooks/useMountEffect" ;
33import { type DomEditSelection } from "./domEditing" ;
4+ import { useMarqueeGestures } from "./marqueeCommit" ;
45import { resolveDomEditGroupOverlayRect } from "./domEditOverlayGeometry" ;
56import {
67 type BlockedMoveState ,
@@ -67,8 +68,10 @@ interface DomEditOverlayProps {
6768 gridSpacing ?: number ;
6869 recordingState ?: GestureRecordingState ;
6970 onToggleRecording ?: ( ) => void ;
71+ onMarqueeSelect ?: ( selections : DomEditSelection [ ] , additive : boolean ) => void ;
7072}
7173
74+ // fallow-ignore-next-line complexity
7275export const DomEditOverlay = memo ( function DomEditOverlay ( {
7376 iframeRef,
7477 activeCompositionPath,
@@ -88,9 +91,12 @@ export const DomEditOverlay = memo(function DomEditOverlay({
8891 onGroupPathOffsetCommit,
8992 onBoxSizeCommit,
9093 onRotationCommit,
94+ onMarqueeSelect,
9195} : DomEditOverlayProps ) {
9296 const overlayRef = useRef < HTMLDivElement | null > ( null ) ;
9397 const boxRef = useRef < HTMLDivElement | null > ( null ) ;
98+ const onMarqueeSelectRef = useRef ( onMarqueeSelect ) ;
99+ onMarqueeSelectRef . current = onMarqueeSelect ;
94100
95101 const selectionShapeStyles = ( ( ) => {
96102 const fallback = {
@@ -238,6 +244,15 @@ export const DomEditOverlay = memo(function DomEditOverlay({
238244 snapGuidesRef,
239245 } ) ;
240246
247+ const marquee = useMarqueeGestures ( {
248+ iframeRef,
249+ overlayRef,
250+ activeCompositionPathRef,
251+ onMarqueeSelectRef,
252+ selectionRef,
253+ gestures,
254+ } ) ;
255+
241256 const selectionKey = useMemo ( ( ) => {
242257 if ( ! selection ) return "none" ;
243258 return `${ selection . sourceFile } :${ selection . id ?? selection . selector ?? selection . label } :${ selection . selectorIndex ?? 0 } ` ;
@@ -306,6 +321,36 @@ export const DomEditOverlay = memo(function DomEditOverlay({
306321
307322 const target = event . target as HTMLElement | null ;
308323 if ( target ?. closest ( '[data-dom-edit-selection-box="true"]' ) ) return ;
324+
325+ // Start marquee if clicking on empty canvas (no element under pointer)
326+ if ( ! hoverSelectionRef . current && onMarqueeSelectRef . current && compRect . width > 0 ) {
327+ const overlayEl = overlayRef . current ;
328+ if ( overlayEl ) {
329+ const oRect = overlayEl . getBoundingClientRect ( ) ;
330+ const cx = event . clientX - oRect . left ;
331+ const cy = event . clientY - oRect . top ;
332+ const inComp =
333+ cx >= compRect . left &&
334+ cx <= compRect . left + compRect . width &&
335+ cy >= compRect . top &&
336+ cy <= compRect . top + compRect . height ;
337+ if ( inComp ) {
338+ event . preventDefault ( ) ;
339+ event . stopPropagation ( ) ;
340+ suppressNextOverlayMouseDownRef . current = true ;
341+ ( event . currentTarget as HTMLElement ) . setPointerCapture ( event . pointerId ) ;
342+ marquee . marqueeRef . current = {
343+ startX : cx ,
344+ startY : cy ,
345+ currentX : cx ,
346+ currentY : cy ,
347+ pointerId : event . pointerId ,
348+ pastThreshold : false ,
349+ } ;
350+ return ;
351+ }
352+ }
353+ }
309354 } ;
310355
311356 const handleBoxClick = ( event : React . MouseEvent < HTMLDivElement > ) => {
@@ -332,15 +377,16 @@ export const DomEditOverlay = memo(function DomEditOverlay({
332377 className = "absolute inset-0 z-10 pointer-events-auto outline-none"
333378 tabIndex = { - 1 }
334379 aria-label = "Composition canvas"
380+ style = { marquee . marqueeRef . current ?. pastThreshold ? { cursor : "crosshair" } : undefined }
335381 onPointerDownCapture = { ( event ) =>
336382 focusDomEditOverlayElement ( event . currentTarget as FocusableDomEditOverlay )
337383 }
338384 onPointerDown = { handleOverlayPointerDown }
339385 onMouseDown = { handleOverlayMouseDown }
340- onPointerMove = { gestures . onPointerMove }
386+ onPointerMove = { marquee . onPointerMove }
341387 onPointerLeave = { ( ) => onCanvasPointerLeaveRef . current ( ) }
342- onPointerUp = { gestures . onPointerUp }
343- onPointerCancel = { ( ) => gestures . clearPointerState ( selectionRef ) }
388+ onPointerUp = { marquee . onPointerUp }
389+ onPointerCancel = { marquee . onPointerCancel }
344390 >
345391 { hoverSelection && hoverRect && compRect . width > 0 && (
346392 < div
@@ -496,6 +542,18 @@ export const DomEditOverlay = memo(function DomEditOverlay({
496542 } }
497543 />
498544 ) ) }
545+ { marquee . marqueeRect && (
546+ < div
547+ aria-hidden = "true"
548+ className = "pointer-events-none absolute border border-dashed border-studio-accent bg-studio-accent/10"
549+ style = { {
550+ left : marquee . marqueeRect . left ,
551+ top : marquee . marqueeRect . top ,
552+ width : marquee . marqueeRect . width ,
553+ height : marquee . marqueeRect . height ,
554+ } }
555+ />
556+ ) }
499557 < GridOverlay
500558 visible = { gridVisible }
501559 spacing = { gridSpacing }
0 commit comments