diff --git a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx index 79b6ae8d7..3c0fae5a0 100644 --- a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx +++ b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx @@ -8,19 +8,15 @@ * - 拖动时实时显示裁剪预览(入点/出点/时长) * - 右键片段弹出菜单:分割 / 恢复原始长度 / 删除 */ -import React, { useState, useRef, useCallback, useEffect, useLayoutEffect, useMemo } from "react" +import React, { useState, useRef, useCallback, useEffect } from "react" import type { ClipData, ClipType, TrimConfig } from "../types" import { DEFAULT_PIXELS_PER_SECOND, - MIN_PIXELS_PER_SECOND, - MAX_PIXELS_PER_SECOND, - ZOOM_STEP, - MIN_TRIM_DURATION, - DEFAULT_ADD_DURATION, - MIN_ADD_DURATION, - MAX_ADD_DURATION, } from "../constants/timeline" import { formatTime } from "../utils/timeline" +import { useClipDrag } from "../hooks/useClipDrag" +import { useTrimDrag } from "../hooks/useTrimDrag" +import { useTimelineMenus } from "../hooks/useTimelineMenus" import { ClipCard } from "./timeline/ClipCard" import { TimeRuler } from "./timeline/TimeRuler" import { AddClipPicker } from "./timeline/AddClipPicker" @@ -53,25 +49,6 @@ interface TimelinePanelProps { totalDuration?: number } -/** 裁剪拖拽方向 */ -type TrimDirection = "left" | "right" - -/** 裁剪拖拽状态 */ -interface TrimDragState { - clipId: string - direction: TrimDirection - startX: number - originalTrim: TrimConfig - originalDuration: number -} - -/** 右键菜单状态 */ -interface ContextMenuState { - x: number - y: number - clipId: string -} - const TimelinePanel: React.FC = ({ clips, selectedClipId, @@ -89,147 +66,51 @@ const TimelinePanel: React.FC = ({ onSeek, totalDuration: totalDurationProp, }) => { - const [dragIdx, setDragIdx] = useState(null) - const [dragOverIdx, setDragOverIdx] = useState(null) - const dragRef = useRef(null) - const [showAddPicker, setShowAddPicker] = useState(false) - const pickerRef = useRef(null) - const addCardRef = useRef(null) - const [pickerPos, setPickerPos] = useState<{ top: number; right: number }>({ - top: 0, - right: 0, - }) + /* ── 缩放 & 时长 ── */ + const pps = pixelsPerSecond ?? DEFAULT_PIXELS_PER_SECOND + const totalDuration = totalDurationProp ?? clips.reduce((s, c) => s + c.duration, 0) - /* ── 裁剪拖拽状态 ── */ - const [trimDrag, setTrimDrag] = useState(null) - const [trimPreview, setTrimPreview] = useState<{ - clipId: string - startTime: number - endTime: number - duration: number - x: number - y: number - } | null>(null) + /* ── 裁剪拖拽 ── */ + const { trimDrag, trimPreview, handleTrimHandleMouseDown } = useTrimDrag(clips, pps, onClipTrim) - /* ── 右键菜单 ── */ - const [contextMenu, setContextMenu] = useState(null) - const contextMenuRef = useRef(null) + /* ── 片段拖拽排序 ── */ + const { + dragIdx, + dragOverIdx, + handleDragStart, + handleDragOver, + handleDragEnd, + handleDrop, + handleEmptyDragOver, + } = useClipDrag(onClipReorder, !!trimDrag) - /* ── 悬停的片段 ID(显示裁剪手柄) ── */ - const [hoveredClipId, setHoveredClipId] = useState(null) + /* ── 菜单 & 面板 ── */ + const { + contextMenu, + contextMenuRef, + handleContextMenu, + handleContextSplit, + handleContextResetTrim, + handleContextDelete, + showAddPicker, + pickerRef, + addCardRef, + pickerPos, + availableTypes, + addType, + addDuration, + setAddType, + setAddDuration, + handleTogglePicker, + handleConfirmAdd, + hoveredClipId, + setHoveredClipId, + } = useTimelineMenus(clips, currentMode, onAddClip, onClipSplit, onClipResetTrim, onClipRemove) /* ── 播放头拖拽状态 ── */ const [playheadDragging, setPlayheadDragging] = useState(false) const trackRef = useRef(null) - /* ── 根据模式决定可选类型 ── */ - const availableTypes: ClipType[] = useMemo( - () => - currentMode === "voice_over" ? ["voice"] : currentMode === "pip" ? ["pip"] : ["voice", "pip"], // voice_pip / one_take / 默认 - [currentMode], - ) - - /* ── 默认添加类型:跟随模式 ── */ - const defaultAddType: ClipType = useMemo(() => { - if (currentMode === "voice_over") return "voice" - if (currentMode === "pip") return "pip" - return "voice" - }, [currentMode]) - - /* ── "+" 卡片:类型+时长选择状态 ── */ - const [addType, setAddType] = useState(defaultAddType) - const [addDuration, setAddDuration] = useState(5) - - /* ── 模式切换时自动同步默认添加类型 ── */ - useEffect(() => { - if (!availableTypes.includes(addType)) { - setAddType(defaultAddType) - } - }, [currentMode, addType, availableTypes, defaultAddType]) - - /* ── 面板尺寸 ── */ - const PICKER_W = 240 - const GAP = 6 - - /* ── 计算 picker 初始位置 ── */ - const updatePickerPosition = useCallback(() => { - if (!addCardRef.current) return - const rect = addCardRef.current.getBoundingClientRect() - const vw = window.innerWidth - const roughHeight = 180 - let top = rect.top - GAP - roughHeight - if (top < 8) top = 8 - let right = vw - rect.right - if (rect.right - PICKER_W < 8) { - right = vw - PICKER_W - 8 - } - setPickerPos({ top, right }) - }, []) - - const handleTogglePicker = () => { - if (!showAddPicker) { - const defaultType = - currentMode === "pip" ? "pip" : currentMode === "voice_over" ? "voice" : "voice" - setAddType(defaultType) - updatePickerPosition() - } - setShowAddPicker((v) => !v) - } - - /* ── 渲染后精确边界校正 ── */ - useLayoutEffect(() => { - if (!showAddPicker || !pickerRef.current || !addCardRef.current) return - const pickerEl = pickerRef.current - const addRect = addCardRef.current.getBoundingClientRect() - const pickerH = pickerEl.offsetHeight - const vh = window.innerHeight - const vw = window.innerWidth - let top = addRect.top - GAP - pickerH - if (top < 8) { - top = addRect.bottom + GAP - if (top + pickerH > vh - 8) { - top = vh - 8 - pickerH - if (top < 8) top = 8 - } - } - let right = vw - addRect.right - const pickerRect = pickerEl.getBoundingClientRect() - if (pickerRect.left < 8) { - right = vw - PICKER_W - 8 - } - setPickerPos({ top, right }) - }, [showAddPicker]) - - /* ── 点击外部关闭添加面板 ── */ - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (pickerRef.current && !pickerRef.current.contains(e.target as Node)) { - setShowAddPicker(false) - } - } - if (showAddPicker) { - document.addEventListener("mousedown", handleClickOutside) - } - return () => document.removeEventListener("mousedown", handleClickOutside) - }, [showAddPicker]) - - /* ── 点击外部关闭右键菜单 ── */ - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) { - setContextMenu(null) - } - } - if (contextMenu) { - document.addEventListener("mousedown", handleClickOutside) - } - return () => document.removeEventListener("mousedown", handleClickOutside) - }, [contextMenu]) - - /* ── 缩放 & 时长 ── */ - const pps = pixelsPerSecond ?? 40 - const totalDuration = totalDurationProp ?? clips.reduce((s, c) => s + c.duration, 0) - /* ── 播放头拖拽全局 mousemove/mouseup ── */ useEffect(() => { if (!playheadDragging) return @@ -273,164 +154,6 @@ const TimelinePanel: React.FC = ({ setPlayheadDragging(true) }, []) - /* ── 确认添加片段 ── */ - const handleConfirmAdd = () => { - onAddClip(addType, addDuration) - setShowAddPicker(false) - } - - /* ── 片段拖拽排序 ── */ - const handleDragStart = (e: React.DragEvent, idx: number) => { - // 如果正在裁剪拖拽,不允许排序拖拽 - if (trimDrag) return - dragRef.current = idx - setDragIdx(idx) - e.dataTransfer.setData("application/x-clip-drag", String(idx)) - e.dataTransfer.effectAllowed = "move" - } - - const handleDragOver = (e: React.DragEvent, idx: number) => { - e.preventDefault() - e.dataTransfer.dropEffect = "move" - setDragOverIdx(idx) - } - - const handleDragEnd = () => { - dragRef.current = null - setDragIdx(null) - setDragOverIdx(null) - } - - const handleDrop = (e: React.DragEvent, toIdx: number) => { - e.preventDefault() - setDragOverIdx(null) - const fromStr = e.dataTransfer.getData("application/x-clip-drag") - if (fromStr !== "") { - const fromIdx = Number(fromStr) - if (fromIdx !== toIdx) { - onClipReorder(fromIdx, toIdx) - } - } - } - - /* ── 空轨道区域不接受素材拖入 ── */ - const handleEmptyDragOver = (e: React.DragEvent) => { - e.preventDefault() - } - - /* ── 裁剪手柄拖拽 ── */ - const handleTrimHandleMouseDown = useCallback( - (e: React.MouseEvent, clipId: string, direction: TrimDirection) => { - e.preventDefault() - e.stopPropagation() - const clip = clips.find((c) => c.id === clipId) - if (!clip) return - - const trim: TrimConfig = clip.trim_config ?? { - start_time: 0, - end_time: clip.duration, - original_duration: clip.duration, - } - - setTrimDrag({ - clipId, - direction, - startX: e.clientX, - originalTrim: { ...trim }, - originalDuration: clip.duration, - }) - }, - [clips], - ) - - /* ── 裁剪拖拽全局 mousemove/mouseup ── */ - useEffect(() => { - if (!trimDrag) return - - const PX_PER_SECOND = pixelsPerSecond ?? 40 // 与缩放级别同步 - - const handleMouseMove = (e: MouseEvent) => { - const dx = e.clientX - trimDrag.startX - const dtSec = dx / PX_PER_SECOND - const clip = clips.find((c) => c.id === trimDrag.clipId) - if (!clip) return - - const origTrim = trimDrag.originalTrim - const origDur = origTrim.original_duration ?? trimDrag.originalDuration - let newStart = origTrim.start_time - let newEnd = origTrim.end_time - - if (trimDrag.direction === "left") { - // 左手柄:调整入点 - newStart = Math.max(0, Math.min(origTrim.start_time + dtSec, newEnd - 1)) - } else { - // 右手柄:调整出点 - newEnd = Math.max(origTrim.start_time + 1, Math.min(origTrim.end_time + dtSec, origDur)) - } - - const newDuration = Math.round((newEnd - newStart) * 10) / 10 - - setTrimPreview({ - clipId: trimDrag.clipId, - startTime: Math.round(newStart * 10) / 10, - endTime: Math.round(newEnd * 10) / 10, - duration: newDuration, - x: e.clientX, - y: e.clientY, - }) - } - - const handleMouseUp = () => { - if (trimPreview && trimPreview.clipId === trimDrag.clipId && onClipTrim) { - const newTrim: TrimConfig = { - start_time: trimPreview.startTime, - end_time: trimPreview.endTime, - original_duration: trimDrag.originalTrim.original_duration ?? trimDrag.originalDuration, - } - onClipTrim(trimDrag.clipId, newTrim, trimPreview.duration) - } - setTrimDrag(null) - setTrimPreview(null) - } - - document.addEventListener("mousemove", handleMouseMove) - document.addEventListener("mouseup", handleMouseUp) - return () => { - document.removeEventListener("mousemove", handleMouseMove) - document.removeEventListener("mouseup", handleMouseUp) - } - }, [trimDrag, trimPreview, clips, onClipTrim, pixelsPerSecond]) - - /* ── 右键菜单 ── */ - const handleContextMenu = useCallback((e: React.MouseEvent, clipId: string) => { - e.preventDefault() - e.stopPropagation() - setContextMenu({ x: e.clientX, y: e.clientY, clipId }) - }, []) - - /* ── 右键菜单操作 ── */ - const handleContextSplit = useCallback(() => { - if (!contextMenu) return - if (onClipSplit) { - onClipSplit(contextMenu.clipId, 0.5) // 在中间分割 - } - setContextMenu(null) - }, [contextMenu, onClipSplit]) - - const handleContextResetTrim = useCallback(() => { - if (!contextMenu) return - if (onClipResetTrim) { - onClipResetTrim(contextMenu.clipId) - } - setContextMenu(null) - }, [contextMenu, onClipResetTrim]) - - const handleContextDelete = useCallback(() => { - if (!contextMenu) return - onClipRemove(contextMenu.clipId) - setContextMenu(null) - }, [contextMenu, onClipRemove]) - const trackWidth = Math.max(totalDuration * pps, 300) return ( diff --git a/apps/web/src/pages/editing-planner/hooks/useClipDrag.ts b/apps/web/src/pages/editing-planner/hooks/useClipDrag.ts new file mode 100644 index 000000000..a93e88f7a --- /dev/null +++ b/apps/web/src/pages/editing-planner/hooks/useClipDrag.ts @@ -0,0 +1,64 @@ +import { useState, useCallback } from "react" + +/** + * 片段拖拽排序 Hook + * 支持 HTML5 原生拖拽,实时高亮拖拽位置 + */ +export const useClipDrag = ( + onClipReorder: (fromIdx: number, toIdx: number) => void, + disabled?: boolean, +) => { + const [dragIdx, setDragIdx] = useState(null) + const [dragOverIdx, setDragOverIdx] = useState(null) + + const handleDragStart = useCallback( + (e: React.DragEvent, idx: number) => { + if (disabled) return + setDragIdx(idx) + e.dataTransfer.setData("application/x-clip-drag", String(idx)) + e.dataTransfer.effectAllowed = "move" + }, + [disabled], + ) + + const handleDragOver = useCallback((e: React.DragEvent, idx: number) => { + e.preventDefault() + e.dataTransfer.dropEffect = "move" + setDragOverIdx(idx) + }, []) + + const handleDragEnd = useCallback(() => { + setDragIdx(null) + setDragOverIdx(null) + }, []) + + const handleDrop = useCallback( + (e: React.DragEvent, toIdx: number) => { + e.preventDefault() + setDragOverIdx(null) + const fromStr = e.dataTransfer.getData("application/x-clip-drag") + if (fromStr !== "") { + const fromIdx = Number(fromStr) + if (fromIdx !== toIdx) { + onClipReorder(fromIdx, toIdx) + } + } + setDragIdx(null) + }, + [onClipReorder], + ) + + const handleEmptyDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + }, []) + + return { + dragIdx, + dragOverIdx, + handleDragStart, + handleDragOver, + handleDragEnd, + handleDrop, + handleEmptyDragOver, + } +} diff --git a/apps/web/src/pages/editing-planner/hooks/useTimelineMenus.ts b/apps/web/src/pages/editing-planner/hooks/useTimelineMenus.ts new file mode 100644 index 000000000..fa2dc18c9 --- /dev/null +++ b/apps/web/src/pages/editing-planner/hooks/useTimelineMenus.ts @@ -0,0 +1,202 @@ +import { useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect } from "react" +import type { ClipData, ClipType, TrimConfig } from "../types" +import { + DEFAULT_ADD_DURATION, + MIN_ADD_DURATION, + MAX_ADD_DURATION, + ADD_PICKER_WIDTH, + TRACK_GAP, +} from "../constants/timeline" + +interface ContextMenuState { + x: number + y: number + clipId: string +} + +/** + * 时间线菜单 Hook + * 管理右键菜单和添加片段面板的状态与交互 + */ +export const useTimelineMenus = ( + clips: ClipData[], + currentMode: string, + onAddClip: (type: ClipType, duration: number) => void, + onClipSplit?: (clipId: string, splitRatio: number) => void, + onClipResetTrim?: (clipId: string) => void, + onClipRemove?: (clipId: string) => void, +) => { + /* ── 右键菜单 ── */ + const [contextMenu, setContextMenu] = useState(null) + const contextMenuRef = useRef(null) + + /* ── 添加片段面板 ── */ + const [showAddPicker, setShowAddPicker] = useState(false) + const pickerRef = useRef(null) + const addCardRef = useRef(null) + const [pickerPos, setPickerPos] = useState<{ top: number; right: number }>({ top: 0, right: 0 }) + + /* ── 悬停的片段 ID(显示裁剪手柄) ── */ + const [hoveredClipId, setHoveredClipId] = useState(null) + + /* ── 根据模式决定可选类型 ── */ + const availableTypes: ClipType[] = useMemo( + () => + currentMode === "voice_over" + ? ["voice"] + : currentMode === "pip" + ? ["pip"] + : ["voice", "pip"], + [currentMode], + ) + + /* ── 默认添加类型:跟随模式 ── */ + const defaultAddType: ClipType = useMemo(() => { + if (currentMode === "voice_over") return "voice" + if (currentMode === "pip") return "pip" + return "voice" + }, [currentMode]) + + /* ── "+" 卡片:类型+时长选择状态 ── */ + const [addType, setAddType] = useState(defaultAddType) + const [addDuration, setAddDuration] = useState(DEFAULT_ADD_DURATION) + + /* ── 模式切换时自动同步默认添加类型 ── */ + useEffect(() => { + if (!availableTypes.includes(addType)) { + setAddType(defaultAddType) + } + }, [currentMode, addType, availableTypes, defaultAddType]) + + /* ── 计算 picker 初始位置 ── */ + const updatePickerPosition = useCallback(() => { + if (!addCardRef.current) return + const rect = addCardRef.current.getBoundingClientRect() + const vw = window.innerWidth + const roughHeight = 180 + let top = rect.top - TRACK_GAP - roughHeight + if (top < 8) top = 8 + let right = vw - rect.right + if (rect.right - ADD_PICKER_WIDTH < 8) { + right = vw - ADD_PICKER_WIDTH - 8 + } + setPickerPos({ top, right }) + }, []) + + const handleTogglePicker = useCallback(() => { + if (!showAddPicker) { + setAddType(defaultAddType) + updatePickerPosition() + } + setShowAddPicker((v) => !v) + }, [showAddPicker, defaultAddType, updatePickerPosition]) + + /* ── 渲染后精确边界校正 ── */ + useLayoutEffect(() => { + if (!showAddPicker || !pickerRef.current || !addCardRef.current) return + const pickerEl = pickerRef.current + const addRect = addCardRef.current.getBoundingClientRect() + const pickerH = pickerEl.offsetHeight + const vh = window.innerHeight + const vw = window.innerWidth + + let top = addRect.top - TRACK_GAP - pickerH + if (top < 8) { + top = addRect.bottom + TRACK_GAP + if (top + pickerH > vh - 8) { + top = vh - 8 - pickerH + if (top < 8) top = 8 + } + } + + let right = vw - addRect.right + const pickerRect = pickerEl.getBoundingClientRect() + if (pickerRect.left < 8) { + right = vw - ADD_PICKER_WIDTH - 8 + } + + setPickerPos({ top, right }) + }, [showAddPicker]) + + /* ── 点击外部关闭添加面板 ── */ + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (pickerRef.current && !pickerRef.current.contains(e.target as Node)) { + setShowAddPicker(false) + } + } + if (showAddPicker) { + document.addEventListener("mousedown", handleClickOutside) + } + return () => document.removeEventListener("mousedown", handleClickOutside) + }, [showAddPicker]) + + /* ── 点击外部关闭右键菜单 ── */ + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) { + setContextMenu(null) + } + } + if (contextMenu) { + document.addEventListener("mousedown", handleClickOutside) + } + return () => document.removeEventListener("mousedown", handleClickOutside) + }, [contextMenu]) + + /* ── 确认添加片段 ── */ + const handleConfirmAdd = useCallback(() => { + onAddClip(addType, addDuration) + setShowAddPicker(false) + }, [onAddClip, addType, addDuration]) + + /* ── 右键菜单操作 ── */ + const handleContextMenu = useCallback((e: React.MouseEvent, clipId: string) => { + e.preventDefault() + e.stopPropagation() + setContextMenu({ x: e.clientX, y: e.clientY, clipId }) + }, []) + + const handleContextSplit = useCallback(() => { + if (!contextMenu) return + onClipSplit?.(contextMenu.clipId, 0.5) + setContextMenu(null) + }, [contextMenu, onClipSplit]) + + const handleContextResetTrim = useCallback(() => { + if (!contextMenu) return + onClipResetTrim?.(contextMenu.clipId) + setContextMenu(null) + }, [contextMenu, onClipResetTrim]) + + const handleContextDelete = useCallback(() => { + if (!contextMenu) return + onClipRemove?.(contextMenu.clipId) + setContextMenu(null) + }, [contextMenu, onClipRemove]) + + return { + // 右键菜单 + contextMenu, + contextMenuRef, + handleContextMenu, + handleContextSplit, + handleContextResetTrim, + handleContextDelete, + // 添加面板 + showAddPicker, + pickerRef, + addCardRef, + pickerPos, + availableTypes, + addType, + addDuration, + setAddType, + setAddDuration, + handleTogglePicker, + handleConfirmAdd, + // 悬停状态 + hoveredClipId, + setHoveredClipId, + } +} diff --git a/apps/web/src/pages/editing-planner/hooks/useTrimDrag.ts b/apps/web/src/pages/editing-planner/hooks/useTrimDrag.ts new file mode 100644 index 000000000..f1ca6127b --- /dev/null +++ b/apps/web/src/pages/editing-planner/hooks/useTrimDrag.ts @@ -0,0 +1,118 @@ +import { useState, useCallback, useEffect } from "react" +import type { ClipData, TrimConfig } from "../types" + +interface TrimDragState { + clipId: string + direction: "left" | "right" + startX: number + originalTrim: TrimConfig + originalDuration: number +} + +interface TrimPreviewState { + clipId: string + startTime: number + endTime: number + duration: number + x: number + y: number +} + +const MIN_TRIM_DURATION = 1 + +/** + * 裁剪拖拽 Hook + * 拖拽片段两端手柄调整入点/出点,实时显示预览 + */ +export const useTrimDrag = ( + clips: ClipData[], + pps: number, + onClipTrim?: (clipId: string, trimConfig: TrimConfig, newDuration: number) => void, +) => { + const [trimDrag, setTrimDrag] = useState(null) + const [trimPreview, setTrimPreview] = useState(null) + + const handleTrimHandleMouseDown = useCallback( + (e: React.MouseEvent, clipId: string, direction: "left" | "right") => { + e.preventDefault() + e.stopPropagation() + const clip = clips.find((c) => c.id === clipId) + if (!clip) return + + const trim: TrimConfig = clip.trim_config ?? { + start_time: 0, + end_time: clip.duration, + original_duration: clip.duration, + } + + setTrimDrag({ + clipId, + direction, + startX: e.clientX, + originalTrim: { ...trim }, + originalDuration: clip.duration, + }) + }, + [clips], + ) + + /* ── 裁剪拖拽全局 mousemove/mouseup ── */ + useEffect(() => { + if (!trimDrag) return + + const handleMouseMove = (e: MouseEvent) => { + const dx = e.clientX - trimDrag.startX + const dtSec = dx / pps + const clip = clips.find((c) => c.id === trimDrag.clipId) + if (!clip) return + + const origTrim = trimDrag.originalTrim + const origDur = origTrim.original_duration ?? trimDrag.originalDuration + let newStart = origTrim.start_time + let newEnd = origTrim.end_time + + if (trimDrag.direction === "left") { + newStart = Math.max(0, Math.min(origTrim.start_time + dtSec, newEnd - MIN_TRIM_DURATION)) + } else { + newEnd = Math.max(origTrim.start_time + MIN_TRIM_DURATION, Math.min(origTrim.end_time + dtSec, origDur)) + } + + const newDuration = Math.round((newEnd - newStart) * 10) / 10 + + setTrimPreview({ + clipId: trimDrag.clipId, + startTime: Math.round(newStart * 10) / 10, + endTime: Math.round(newEnd * 10) / 10, + duration: newDuration, + x: e.clientX, + y: e.clientY, + }) + } + + const handleMouseUp = () => { + if (trimPreview && trimPreview.clipId === trimDrag.clipId && onClipTrim) { + const newTrim: TrimConfig = { + start_time: trimPreview.startTime, + end_time: trimPreview.endTime, + original_duration: trimDrag.originalTrim.original_duration ?? trimDrag.originalDuration, + } + onClipTrim(trimDrag.clipId, newTrim, trimPreview.duration) + } + setTrimDrag(null) + setTrimPreview(null) + } + + document.addEventListener("mousemove", handleMouseMove) + document.addEventListener("mouseup", handleMouseUp) + return () => { + document.removeEventListener("mousemove", handleMouseMove) + document.removeEventListener("mouseup", handleMouseUp) + } + }, [trimDrag, trimPreview, clips, pps, onClipTrim]) + + return { + trimDrag, + trimPreview, + handleTrimHandleMouseDown, + } +}