import { computed, onMounted, onUnmounted, ref } from 'vue' const DRAG_THRESHOLD = 6 type FabPosition = { x: number; y: number } type DraggableFloatingOptions = { width?: number height?: number defaultBottomOffset?: number defaultRightOffset?: number } export function useDraggableFloatingButton( storageKey: string, options: DraggableFloatingOptions = {} ) { const width = options.width ?? 96 const height = options.height ?? 44 const defaultBottomOffset = options.defaultBottomOffset ?? 92 const defaultRightOffset = options.defaultRightOffset ?? 20 const pos = ref({ x: 0, y: 0 }) const dragging = ref(false) let pointerId: number | null = null let startX = 0 let startY = 0 let originX = 0 let originY = 0 let hasMoved = false const fabStyle = computed(() => ({ left: `${pos.value.x}px`, top: `${pos.value.y}px`, width: `${width}px`, touchAction: 'none' })) function getViewportWidth() { return Math.min(window.innerWidth, 426) } function getHorizontalOffset() { const vw = window.innerWidth const contentWidth = getViewportWidth() return Math.max(0, (vw - contentWidth) / 2) } function resetToDefault() { const vh = window.innerHeight const offsetX = getHorizontalOffset() const contentWidth = getViewportWidth() pos.value = { x: offsetX + contentWidth - width - defaultRightOffset, y: vh - defaultBottomOffset - height } } function clampToViewport() { const vw = window.innerWidth const vh = window.innerHeight pos.value = { x: Math.max(0, Math.min(vw - width, pos.value.x)), y: Math.max(0, Math.min(vh - height, pos.value.y)) } } function loadPosition() { try { const raw = localStorage.getItem(storageKey) if (!raw) return false const parsed = JSON.parse(raw) as FabPosition if (typeof parsed?.x === 'number' && typeof parsed?.y === 'number') { pos.value = parsed clampToViewport() return true } } catch { // ignore } return false } function savePosition() { localStorage.setItem(storageKey, JSON.stringify(pos.value)) } function handleResize() { clampToViewport() savePosition() } function handlePointerDown(event: PointerEvent) { dragging.value = true hasMoved = false pointerId = event.pointerId startX = event.clientX startY = event.clientY originX = pos.value.x originY = pos.value.y ;(event.currentTarget as HTMLElement)?.setPointerCapture?.(event.pointerId) } function handlePointerMove(event: PointerEvent) { if (!dragging.value || pointerId !== event.pointerId) return const dx = event.clientX - startX const dy = event.clientY - startY if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) { hasMoved = true } pos.value = { x: originX + dx, y: originY + dy } clampToViewport() } function finishPointer(event: PointerEvent) { if (!dragging.value || pointerId !== event.pointerId) return false dragging.value = false pointerId = null savePosition() const clicked = !hasMoved hasMoved = false return clicked } function handlePointerUp(event: PointerEvent) { return finishPointer(event) } function handlePointerCancel(event: PointerEvent) { finishPointer(event) return false } onMounted(() => { if (!loadPosition()) { resetToDefault() } window.addEventListener('resize', handleResize) }) onUnmounted(() => { window.removeEventListener('resize', handleResize) }) return { dragging, fabStyle, handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel, resetToDefault } }