export type DraggableList = { id: string folder_id: string | null is_inbox: boolean } export type ListDragPointer = { pointerId: number clientX: number clientY: number captureTarget: Element | null } export function snapshotListDragPointer(event: PointerEvent): ListDragPointer { return { pointerId: event.pointerId, clientX: event.clientX, clientY: event.clientY, captureTarget: event.currentTarget as Element | null, } } export function captureListDragPointer(pointer: ListDragPointer) { try { pointer.captureTarget?.setPointerCapture(pointer.pointerId) } catch { /* synthetic events */ } } export function hasExceededLongPressMovement( start: { x: number; y: number }, current: { x: number; y: number }, threshold: number, ) { return Math.hypot(current.x - start.x, current.y - start.y) > threshold } export function getAdjacentListMove( items: T[], sourceId: string, direction: 'up' | 'down', ): { targetId: string; placement: 'before' | 'after' } | null { const source = items.find((item) => item.id === sourceId) if (!source || source.is_inbox) return null const scope = items.filter((item) => !item.is_inbox && item.folder_id === source.folder_id) const index = scope.findIndex((item) => item.id === sourceId) const target = scope[index + (direction === 'up' ? -1 : 1)] if (!target) return null return { targetId: target.id, placement: direction === 'up' ? 'before' : 'after' } } export function moveListToScope( items: T[], sourceId: string, folderId: string | null, targetId?: string, placement: 'before' | 'after' = 'after', ) { const source = items.find((item) => item.id === sourceId) if (!source || source.is_inbox) return { items, orderedIds: [] as string[] } const withoutSource = items.filter((item) => item.id !== sourceId) const moved = { ...source, folder_id: folderId } const scope = withoutSource.filter((item) => !item.is_inbox && item.folder_id === folderId) const targetIndex = targetId ? scope.findIndex((item) => item.id === targetId) : -1 const insertAt = targetIndex < 0 ? scope.length : targetIndex + (placement === 'after' ? 1 : 0) scope.splice(insertAt, 0, moved) const iterator = scope[Symbol.iterator]() const next = withoutSource.map((item) => { if (item.is_inbox || item.folder_id !== folderId) return item return iterator.next().value! }) const remaining = [...iterator] if (remaining.length) next.push(...remaining) return { items: next, orderedIds: scope.map((item) => item.id) } }