82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useLayoutEffect, useRef, useState } from "react"
|
|
import { createPortal } from "react-dom"
|
|
import { Mail } from "lucide-react"
|
|
import { EMAIL_DRAG_RETURN_MS, useEmailDrag } from "@/lib/drag-context"
|
|
import { useIsXs } from "@/hooks/use-xs"
|
|
|
|
export function MoveDragIndicator() {
|
|
const isXs = useIsXs()
|
|
const { state } = useEmailDrag()
|
|
const [mounted, setMounted] = useState(false)
|
|
const elRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
setMounted(true)
|
|
}, [])
|
|
|
|
/**
|
|
* When the user releases the drag outside any valid target the provider
|
|
* flips `phase` to "returning". We then drive a Web-Animations API tween
|
|
* from the current cursor position back to the origin (where the drag
|
|
* started) while fading out, to clearly signal "no move happened".
|
|
*
|
|
* `useLayoutEffect` (not `useEffect`) so the animation is queued in the
|
|
* same paint as the phase flip — otherwise the indicator visibly pauses
|
|
* for one frame at the release point before animating.
|
|
*
|
|
* We animate the `translate` CSS property (NOT `transform`) so the
|
|
* inline `transform: translate(-50%, -50%)` used for centering stays
|
|
* untouched and composes additively with the animated translation.
|
|
*/
|
|
useLayoutEffect(() => {
|
|
if (!state) return
|
|
if (state.phase !== "returning") return
|
|
const el = elRef.current
|
|
if (!el) return
|
|
const dx = state.originX - state.pointerX
|
|
const dy = state.originY - state.pointerY
|
|
const animation = el.animate(
|
|
[
|
|
{ translate: "0px 0px", opacity: 1 },
|
|
{ translate: `${dx}px ${dy}px`, opacity: 0 },
|
|
],
|
|
{
|
|
duration: EMAIL_DRAG_RETURN_MS,
|
|
easing: "cubic-bezier(0.22, 0.61, 0.36, 1)",
|
|
fill: "forwards",
|
|
}
|
|
)
|
|
return () => {
|
|
animation.cancel()
|
|
}
|
|
}, [state?.phase, state?.originX, state?.originY, state?.pointerX, state?.pointerY])
|
|
|
|
if (isXs || !mounted || !state) return null
|
|
|
|
const count = state.ids.length
|
|
const label =
|
|
count > 1 ? `Déplacer ${count} conversations` : "Déplacer 1 conversation"
|
|
|
|
return createPortal(
|
|
<div
|
|
ref={elRef}
|
|
aria-hidden
|
|
className="pointer-events-none fixed z-100 select-none"
|
|
style={{
|
|
left: state.pointerX,
|
|
top: state.pointerY,
|
|
transform: "translate(-50%, -50%)",
|
|
willChange: "translate, opacity",
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-2 rounded-md bg-[#1a73e8] px-4 py-3 text-sm font-medium text-white shadow-lg">
|
|
<Mail className="size-5 shrink-0" strokeWidth={1.75} />
|
|
<span className="whitespace-nowrap">{label}</span>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
)
|
|
}
|