"use client" import type { ReactNode } from "react" import { useCallback, useMemo, useRef, useState } from "react" import { toast } from "sonner" import { ContextMenu, ContextMenuContent, ContextMenuTrigger, } from "@/components/ui/context-menu" import { Sheet, SheetContent, SheetDescription, SheetTitle } from "@/components/ui/sheet" import { DriveMoveDialog, type DriveFolderPickerMode } from "@/components/drive/drive-move-dialog" import { useDriveMutations } from "@/lib/api/hooks/use-drive-queries" import type { DriveFileInfo } from "@/lib/api/types" import { displayFileName } from "@/lib/drive/display-file-name" import { resolveRenameName } from "@/lib/drive/drive-default-name" import { useLongPress } from "@/hooks/use-long-press" import { useIsMobile } from "@/hooks/use-mobile" import { mergeDriveCardRefs, useDriveCardRefRegistrar } from "@/components/drive/drive-card-ref-context" import { useDriveDragSource } from "@/lib/hooks/use-drive-drag-source" import { useDriveDropTarget } from "@/lib/hooks/use-drive-drop-target" import { guardDriveMenuPointer, isCardOpenSuppressed, isFromDriveMenu, } from "@/lib/drive/drive-menu-guard" import { useDriveUIStore } from "@/lib/stores/drive-ui-store" import { DriveNameDialog } from "@/components/drive/drive-name-dialog" import { DriveFileMenuActions } from "@/components/drive/drive-file-menu-actions" import { MAIL_MENU_SURFACE_CLASS } from "@/lib/mail-chrome-classes" import { cn } from "@/lib/utils" export const DRIVE_MENU_SURFACE_CLASS = cn( MAIL_MENU_SURFACE_CLASS, "z-50 min-w-[12rem] p-1" ) export const DRIVE_CARD_HIGHLIGHT_CLASS = "bg-mail-active" export const DRIVE_DROP_TARGET_CLASS = "ring-2 ring-[#1a73e8] ring-inset bg-[#e8f0fe] dark:bg-primary/20" const MOBILE_TAP_SLOP_PX = 10 export function DriveFileContextMenu({ file, allItems, isTrash, allowShare = true, writable = true, hideFavorite = false, disableDnd = false, mutations: mutationsProp, onDownloadRequest, onOpen, children, className, variant = "default", registerRef, onItemClick, onContextMenuActiveChange, }: { file: DriveFileInfo allItems: DriveFileInfo[] isTrash?: boolean allowShare?: boolean writable?: boolean hideFavorite?: boolean disableDnd?: boolean mutations?: ReturnType onDownloadRequest?: () => void onOpen: () => void children: ReactNode className?: string variant?: "default" | "grid" registerRef?: (el: HTMLDivElement | null) => void onItemClick?: (file: DriveFileInfo, e: React.MouseEvent) => void /** Visual highlight while right-click menu is open (grid cards). */ onContextMenuActiveChange?: (active: boolean) => void }) { const isMobile = useIsMobile() const isGrid = variant === "grid" const registerCardRef = useDriveCardRefRegistrar() const dndEnabled = !isMobile && !isTrash && !disableDnd const isFolder = file.type === "directory" const { dragProps } = useDriveDragSource({ file, allItems, disabled: !dndEnabled }) const { dropProps, canDrop, isOver } = useDriveDropTarget({ folderPath: file.path, disabled: !dndEnabled || !isFolder, }) const [sheetOpen, setSheetOpen] = useState(false) const [renameOpen, setRenameOpen] = useState(false) const [contextMenuOpen, setContextMenuOpen] = useState(false) const [folderPickerMode, setFolderPickerMode] = useState(null) const selectedPaths = useDriveUIStore((s) => s.selectedPaths) const selectionMode = useDriveUIStore((s) => s.selectionMode) const toggleSelect = useDriveUIStore((s) => s.toggleSelect) const enterSelectionMode = useDriveUIStore((s) => s.enterSelectionMode) const setSharePath = useDriveUIStore((s) => s.setSharePath) const mutationsDefault = useDriveMutations() const mutations = mutationsProp ?? mutationsDefault const openRenameDialog = useCallback(() => { guardDriveMenuPointer() window.setTimeout(() => setRenameOpen(true), 0) }, []) /** Context menu / long-press sheet: always the file under cursor, never bulk selection. */ const targets = useMemo(() => [file], [file]) const openSheet = useCallback(() => setSheetOpen(true), []) const longPress = useLongPress(openSheet, { disabled: !isMobile }) const touchStartRef = useRef<{ x: number; y: number } | null>(null) const touchMovedRef = useRef(false) const handleMobilePointerDown = useCallback( (e: React.PointerEvent) => { touchStartRef.current = { x: e.clientX, y: e.clientY } touchMovedRef.current = false longPress.onPointerDown(e) }, [longPress] ) const handleMobilePointerMove = useCallback((e: React.PointerEvent) => { const start = touchStartRef.current if (!start) return const dx = e.clientX - start.x const dy = e.clientY - start.y if (dx * dx + dy * dy > MOBILE_TAP_SLOP_PX * MOBILE_TAP_SLOP_PX) { touchMovedRef.current = true } }, []) const handleMobilePointerEnd = useCallback(() => { touchStartRef.current = null longPress.onPointerUp() }, [longPress]) const handleMobileClickCapture = useCallback( (e: React.MouseEvent) => { longPress.onClickCapture(e) if (touchMovedRef.current) { e.preventDefault() e.stopPropagation() touchMovedRef.current = false } }, [longPress] ) const handleRename = async (input: string) => { const target = targets[0] if (!target) return const newName = resolveRenameName(target, input) if (displayFileName(target.name) === newName) { return } try { await mutations.rename.mutateAsync({ path: target.path, new_name: newName }) toast.success("Renommé") } catch { toast.error("Impossible de renommer") throw new Error("rename failed") } } const shouldIgnoreCardAction = (target: EventTarget | null) => isFromDriveMenu(target) || isCardOpenSuppressed() const handleMobileTap = (e: React.MouseEvent) => { if (shouldIgnoreCardAction(e.target)) { e.preventDefault() e.stopPropagation() return } e.preventDefault() e.stopPropagation() if (selectionMode) { toggleSelect(file.path, !selectedPaths.has(file.path)) return } onOpen() } const touchProps = isMobile ? { onContextMenu: (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() openSheet() }, onPointerDown: handleMobilePointerDown, onPointerMove: handleMobilePointerMove, onPointerUp: handleMobilePointerEnd, onPointerLeave: handleMobilePointerEnd, onPointerCancel: handleMobilePointerEnd, onClickCapture: handleMobileClickCapture, } : {} const handlePointerDownSelect = (e: React.PointerEvent) => { if (e.pointerType === "mouse" && (e.ctrlKey || e.metaKey)) { e.preventDefault() toggleSelect(file.path, !selectedPaths.has(file.path)) } if (isMobile) { handleMobilePointerDown(e) } } const handleClickSelect = (e: React.MouseEvent) => { if (shouldIgnoreCardAction(e.target)) { e.preventDefault() e.stopPropagation() return } if (e.ctrlKey || e.metaKey) { e.preventDefault() e.stopPropagation() toggleSelect(file.path, !selectedPaths.has(file.path)) } } const handleGridClick = (e: React.MouseEvent) => { if (shouldIgnoreCardAction(e.target)) { e.preventDefault() e.stopPropagation() return } onItemClick?.(file, e) } const handleOpenDoubleClick = (e: React.MouseEvent) => { if (shouldIgnoreCardAction(e.target)) { e.preventDefault() e.stopPropagation() return } e.preventDefault() e.stopPropagation() onOpen() } const handleContextMenuOpenChange = useCallback( (open: boolean) => { if (open) { setContextMenuOpen(true) onContextMenuActiveChange?.(true) return } setContextMenuOpen(false) onContextMenuActiveChange?.(false) guardDriveMenuPointer() }, [onContextMenuActiveChange] ) const closeContextMenu = useCallback(() => { handleContextMenuOpenChange(false) }, [handleContextMenuOpenChange]) const mergedRegisterRef = mergeDriveCardRefs(file.path, registerRef, registerCardRef) const isSelected = selectedPaths.has(file.path) const trigger = (
{children}
) const renameTarget = targets.length === 1 ? targets[0] : null const openFolderPicker = (mode: DriveFolderPickerMode) => { setSheetOpen(false) window.setTimeout(() => setFolderPickerMode(mode), 0) } const menuActionsProps = { targets, isTrash, allowShare, writable, hideFavorite, onOpen, setSharePath, mutations, onRenameRequest: openRenameDialog, onMoveRequest: isTrash || disableDnd ? undefined : () => openFolderPicker("move"), onCopyRequest: isTrash || disableDnd ? undefined : () => openFolderPicker("copy"), onDownloadRequest, onEnterSelectionMode: isTrash ? undefined : () => { enterSelectionMode(file.path) }, } const menus = ( <> { if (!open) setFolderPickerMode(null) }} mode={folderPickerMode ?? "move"} sources={targets} /> {targets.length > 1 ? `${targets.length} éléments` : displayFileName(file.name)} {targets.length > 1 ? `Actions pour ${targets.length} éléments sélectionnés.` : `Actions pour ${displayFileName(file.name)}.`}

{targets.length > 1 ? `${targets.length} éléments sélectionnés` : displayFileName(file.name)}

setSheetOpen(false)} onRenameRequest={() => { setSheetOpen(false) openRenameDialog() }} />
) if (isMobile) { return ( <> {trigger} {menus} ) } const contextMenu = ( {trigger} e.preventDefault()} > ) return ( <> {contextMenu} {menus} ) }