import { useCallback, useEffect, useState } from "react"
import { cn } from "@/lib/utils"
import { useEditModeContext } from "../../hooks/use-edit-mode"
import { EDIT_MODES } from "../../types/edit-modes"
interface ClipTrimHandlesProps {
onTrimStart: (edge: "start" | "end", initialX: number) => void
onTrimMove: (deltaX: number) => void
onTrimEnd: (committed: boolean) => void
isSelected: boolean
disabled?: boolean
className?: string
}
export function ClipTrimHandles({
onTrimStart,
onTrimMove,
onTrimEnd,
isSelected,
disabled = false,
className,
}: ClipTrimHandlesProps) {
const { isEditMode } = useEditModeContext()
const [isDragging, setIsDragging] = useState(false)
const [dragEdge, setDragEdge] = useState<"start" | "end" | null>(null)
const [dragStartX, setDragStartX] = useState(0)
// Show handles when in trim/ripple mode or when clip is selected
const showHandles =
(isEditMode(EDIT_MODES.TRIM) || isEditMode(EDIT_MODES.RIPPLE) || isEditMode(EDIT_MODES.ROLL)) &&
(isSelected || isDragging)
const handleMouseDown = useCallback(
(edge: "start" | "end", e: React.MouseEvent) => {
if (disabled) return
e.preventDefault()
e.stopPropagation()
setIsDragging(true)
setDragEdge(edge)
setDragStartX(e.clientX)
onTrimStart(edge, e.clientX)
},
[disabled, onTrimStart],
)
useEffect(() => {
if (!isDragging) return
const handleMouseMove = (e: MouseEvent) => {
const deltaX = e.clientX - dragStartX
onTrimMove(deltaX)
}
const handleMouseUp = (e: MouseEvent) => {
const committed = !e.shiftKey // Shift+release cancels the operation
onTrimEnd(committed)
setIsDragging(false)
setDragEdge(null)
}
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
return () => {
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}
}, [isDragging, dragStartX, onTrimMove, onTrimEnd])
if (!showHandles) return null
return (
<>
{/* Start handle */}
handleMouseDown("start", e)}
data-trim-handle="start"
data-testid="trim-handle-start"
>
{/* End handle */}
handleMouseDown("end", e)}
data-trim-handle="end"
data-testid="trim-handle-end"
>
{/* Visual feedback during trim */}
{isDragging && (
)}
>
)
}
// Ripple handle variant with different styling
interface RippleHandleProps extends ClipTrimHandlesProps {
showRippleIndicator?: boolean
}
export function RippleHandles({ showRippleIndicator = true, ...props }: RippleHandleProps) {
const { isEditMode } = useEditModeContext()
if (!isEditMode(EDIT_MODES.RIPPLE)) {
return
}
return (
{/* Ripple indicator arrow */}
{showRippleIndicator && props.isSelected && (
)}
)
}
// Roll edit handles - connects two adjacent clips
interface RollHandlesProps {
leftClipId: string
rightClipId: string
position: number // Position between clips in pixels
onRollStart: (initialX: number) => void
onRollMove: (deltaX: number) => void
onRollEnd: (committed: boolean) => void
isActive: boolean
disabled?: boolean
}
export function RollHandles({
position,
onRollStart,
onRollMove,
onRollEnd,
isActive,
disabled = false,
}: RollHandlesProps) {
const { isEditMode } = useEditModeContext()
const [isDragging, setIsDragging] = useState(false)
const [dragStartX, setDragStartX] = useState(0)
const handleMouseDown = (e: React.MouseEvent) => {
if (disabled) return
e.preventDefault()
e.stopPropagation()
setIsDragging(true)
setDragStartX(e.clientX)
onRollStart(e.clientX)
}
useEffect(() => {
if (!isDragging) return
const handleMouseMove = (e: MouseEvent) => {
const deltaX = e.clientX - dragStartX
onRollMove(deltaX)
}
const handleMouseUp = (e: MouseEvent) => {
const committed = !e.shiftKey
onRollEnd(committed)
setIsDragging(false)
}
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
return () => {
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}
}, [isDragging, dragStartX, onRollMove, onRollEnd])
if (!isEditMode(EDIT_MODES.ROLL) || !isActive) return null
return (
)
}