mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 02:36:10 +00:00
Gesture recording uses replace-with-keyframes mutation to replace existing position-group tween. Fix N1 sign inversion and N9 wheel startPointer with pointerElementOffset subtraction.
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
import { memo } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import type { TimelineElement } from "../store/playerStore";
|
|
import { canSplitElement } from "../../utils/timelineElementSplit";
|
|
import { useContextMenuDismiss } from "../../hooks/useContextMenuDismiss";
|
|
|
|
interface ClipContextMenuProps {
|
|
x: number;
|
|
y: number;
|
|
element: TimelineElement;
|
|
currentTime: number;
|
|
onClose: () => void;
|
|
onSplit: (element: TimelineElement, splitTime: number) => void;
|
|
onDelete: (element: TimelineElement) => void;
|
|
}
|
|
|
|
export const ClipContextMenu = memo(function ClipContextMenu({
|
|
x,
|
|
y,
|
|
element,
|
|
currentTime,
|
|
onClose,
|
|
onSplit,
|
|
onDelete,
|
|
}: ClipContextMenuProps) {
|
|
const menuRef = useContextMenuDismiss(onClose);
|
|
|
|
const menuWidth = 200;
|
|
const menuHeight = 80;
|
|
const overflowY = y + menuHeight - window.innerHeight;
|
|
const adjustedX = x + menuWidth > window.innerWidth ? x - menuWidth : x;
|
|
const adjustedY = overflowY > 0 ? y - overflowY - 8 : y;
|
|
|
|
const isSplittable = canSplitElement(element) && ["video", "audio", "img"].includes(element.tag);
|
|
const canSplit =
|
|
isSplittable && currentTime > element.start && currentTime < element.start + element.duration;
|
|
|
|
const splitLabel = !isSplittable
|
|
? null
|
|
: canSplit
|
|
? `Split at ${currentTime.toFixed(2)}s`
|
|
: "Split (move playhead inside clip)";
|
|
|
|
return createPortal(
|
|
<div
|
|
ref={menuRef}
|
|
className="fixed z-50 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
|
|
style={{ left: adjustedX, top: adjustedY }}
|
|
>
|
|
{splitLabel && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className={`w-full flex items-center justify-between px-3 py-1.5 text-xs text-left ${
|
|
canSplit
|
|
? "text-neutral-300 hover:bg-neutral-800 cursor-pointer"
|
|
: "text-neutral-600 cursor-not-allowed"
|
|
}`}
|
|
disabled={!canSplit}
|
|
onClick={() => {
|
|
if (canSplit) {
|
|
onSplit(element, currentTime);
|
|
onClose();
|
|
}
|
|
}}
|
|
>
|
|
<span>{splitLabel}</span>
|
|
<span className="text-neutral-500 text-[10px] ml-3">S</span>
|
|
</button>
|
|
<div className="my-1 border-t border-neutral-700/60" />
|
|
</>
|
|
)}
|
|
|
|
<button
|
|
type="button"
|
|
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 cursor-pointer text-left"
|
|
onClick={() => {
|
|
onDelete(element);
|
|
onClose();
|
|
}}
|
|
>
|
|
<span>Delete</span>
|
|
<span className="text-neutral-500 text-[10px] ml-3">⌫</span>
|
|
</button>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
});
|