diff --git a/frontend/src/desktop/CardDragLogic.ts b/frontend/src/desktop/CardDragLogic.ts index c393ce7..2392472 100644 --- a/frontend/src/desktop/CardDragLogic.ts +++ b/frontend/src/desktop/CardDragLogic.ts @@ -4,15 +4,34 @@ export const handleDragStart = (store: LayoutStore, selection: string[], leading const leadingCard = store.items.get(leadingId); if (!leadingCard) return; + // Snap all non-leading cards to the leading card's position + selection.forEach(id => { + if (id === leadingId) return; + const card = store.items.get(id); + if (card) { + const leadingCenterX = leadingCard.x + leadingCard.width / 2; + const leadingCenterY = leadingCard.y + leadingCard.height / 2; + + const targetX = leadingCenterX - card.width / 2; + const targetY = leadingCenterY - card.height / 2; + + card.snapTo(targetX, targetY); + } + }); + selection.forEach(id => { const card = store.items.get(id); if (!card) return; let myOffset = offset; + if (id !== leadingId) { + const pointerX = leadingCard.x + offset.x; + const pointerY = leadingCard.y + offset.y; + myOffset = { - x: offset.x + (leadingCard.x - card.x), - y: offset.y + (leadingCard.y - card.y) + x: pointerX - card.x, + y: pointerY - card.y }; } diff --git a/frontend/src/desktop/LayoutSystem.ts b/frontend/src/desktop/LayoutSystem.ts index c2a8bf1..4d41942 100644 --- a/frontend/src/desktop/LayoutSystem.ts +++ b/frontend/src/desktop/LayoutSystem.ts @@ -179,6 +179,27 @@ export class LayoutCard implements LayoutCardState { }); } + snapTo(x: number, y: number) { + this.update({ x, y }); + + if (!this.ref) + return; + + this.ref.classList.add('desk-item--swoop'); + this.ref.style.transform = `translate3d(${this.x}px, ${this.y}px, 0) rotate(${this.rotation}deg)`; + + const cleanup = () => { + if (this.ref) { + this.ref.classList.remove('desk-item--swoop'); + } + }; + + this.ref.addEventListener('transitionend', cleanup, { once: true }); + + // Safety timeout in case transitionend doesn't fire (e.g. element removed) + setTimeout(cleanup, 350); + } + toSnapshot(): LayoutCardState { return { id: this.id, @@ -252,14 +273,29 @@ export class LayoutStore { for (const card of this.items.values()) { const dx = card.x - x; const dy = card.y - y; - const distance = Math.sqrt(dx * dx + dy * dy); - if (distance < radius + card.outerRadius) { + + // Check if center is within inner radius of top card + // Use squared distance to avoid sqrt + const distSq = dx * dx + dy * dy; + if (distSq < radius * radius) { result.push(card); } } return result; } + getStackBelow(topCard: LayoutCard): string[] { + const candidates = this.getCardsInCircle(topCard.x, topCard.y, topCard.innerRadius); + + return candidates + .filter(other => { + if (other.id === topCard.id) return false; + if (other.z >= topCard.z) return false; + return true; + }) + .map(c => c.id); + } + getSnapshot() { return Array.from(this.items.values()).map(card => card.toSnapshot()); } diff --git a/frontend/src/desktop/useCardPointer.ts b/frontend/src/desktop/useCardPointer.ts index 73cdecf..61cfe3f 100644 --- a/frontend/src/desktop/useCardPointer.ts +++ b/frontend/src/desktop/useCardPointer.ts @@ -35,6 +35,8 @@ export const useCardPointer = ( // Only left click if (e.button !== 0) return; + e.preventDefault(); + (e.target as Element).setPointerCapture(e.pointerId); setState('click'); initialPosition.current = { x: e.clientX, y: e.clientY }; @@ -42,6 +44,8 @@ export const useCardPointer = ( }, []); const onPointerMove = useCallback((e: React.PointerEvent) => { + e.preventDefault(); + const hasModifier = e.metaKey || e.ctrlKey || e.shiftKey; updateState(e); @@ -89,6 +93,8 @@ export const useCardPointer = ( }, [card, state, updateState, isSelected, selection, onSelect]); const onPointerUp = useCallback((e: React.PointerEvent) => { + e.preventDefault(); + const hasModifier = e.metaKey || e.ctrlKey || e.shiftKey; updateState(e); diff --git a/frontend/src/styles/workspace/workspace-cards.css b/frontend/src/styles/workspace/workspace-cards.css index 85a8c6d..0a8bac0 100644 --- a/frontend/src/styles/workspace/workspace-cards.css +++ b/frontend/src/styles/workspace/workspace-cards.css @@ -15,6 +15,10 @@ overflow: hidden; } +.desk-item--swoop { + transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); +} + .desk-item__card img { width: 100%; height: 100%;