OpenCut/apps/web/src/components/landing/handlebars.tsx

190 lines
5.6 KiB
TypeScript

"use client";
import {
type PropsWithChildren,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
type HandlebarsProps = PropsWithChildren;
export function Handlebars({ children }: HandlebarsProps) {
const containerRef = useRef<HTMLDivElement>(null);
const leftHandleRef = useRef<HTMLDivElement>(null);
const rightHandleRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
const [leftHandle, setLeftHandle] = useState(0);
const [rightHandle, setRightHandle] = useState(0);
const widthRef = useRef(0);
const leftHandlePositionRef = useRef(0);
const rightHandlePositionRef = useRef(0);
const dragRef = useRef<{
isDragging: boolean;
side: "left" | "right" | null;
pointerId: number | null;
startX: number;
initialPosition: number;
}>({
isDragging: false,
side: null,
pointerId: null,
startX: 0,
initialPosition: 0,
});
useLayoutEffect(() => {
const el = containerRef.current;
if (!el) return;
const updateWidth = () => {
const newWidth = el.offsetWidth;
setWidth(newWidth);
setRightHandle(newWidth);
};
const observer = new ResizeObserver(updateWidth);
observer.observe(el);
updateWidth();
return () => observer.disconnect();
}, []);
useEffect(() => {
widthRef.current = width;
leftHandlePositionRef.current = leftHandle;
rightHandlePositionRef.current = rightHandle;
}, [leftHandle, rightHandle, width]);
useEffect(() => {
const handlePointerMove = (event: PointerEvent) => {
const { isDragging, side, pointerId, startX, initialPosition } =
dragRef.current;
if (!isDragging) return;
if (pointerId !== null && event.pointerId !== pointerId) return;
if (!side) return;
const deltaX = event.clientX - startX;
if (side === "left") {
const maxLeft = Math.max(0, rightHandlePositionRef.current - 60);
const nextLeftHandle = Math.max(
0,
Math.min(maxLeft, initialPosition + deltaX),
);
setLeftHandle(nextLeftHandle);
return;
}
const minRight = Math.min(
widthRef.current,
leftHandlePositionRef.current + 60,
);
const nextRightHandle = Math.max(
minRight,
Math.min(widthRef.current, initialPosition + deltaX),
);
setRightHandle(nextRightHandle);
};
const handlePointerEnd = (event: PointerEvent) => {
const { pointerId } = dragRef.current;
if (pointerId !== null && event.pointerId !== pointerId) return;
dragRef.current.isDragging = false;
dragRef.current.side = null;
dragRef.current.pointerId = null;
};
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerEnd);
window.addEventListener("pointercancel", handlePointerEnd);
return () => {
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerEnd);
window.removeEventListener("pointercancel", handlePointerEnd);
};
}, []);
const hasMeasuredWidth = width > 0;
const leftGradientPercent = hasMeasuredWidth
? (leftHandle / (width - 10)) * 100
: 0;
const rightGradientPercent = hasMeasuredWidth
? (rightHandle / (width + 10)) * 100
: 100;
const textMask = hasMeasuredWidth
? `linear-gradient(90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0) ${leftGradientPercent}%,
rgba(0, 0, 0) ${leftGradientPercent}%,
rgba(0, 0, 0) ${rightGradientPercent}%,
rgba(255, 255, 255, 0) ${rightGradientPercent}%,
rgba(255, 255, 255, 0) 100%)`
: undefined;
return (
<div className="flex justify-center gap-4 leading-16">
<div ref={containerRef} className="relative mt-0.5 -rotate-[2.76deg]">
<div className="absolute inset-0 z-10 flex size-full justify-between rounded-2xl border border-yellow-500">
<div
ref={leftHandleRef}
className="bg-background absolute left-0 z-20 flex h-full w-7 cursor-ew-resize touch-none items-center justify-center rounded-full border border-yellow-500 select-none"
style={{
translate: `${leftHandle}px 0`,
}}
onPointerDown={(event) => {
event.preventDefault();
leftHandleRef.current?.setPointerCapture(event.pointerId);
dragRef.current.isDragging = true;
dragRef.current.side = "left";
dragRef.current.pointerId = event.pointerId;
dragRef.current.startX = event.clientX;
dragRef.current.initialPosition = leftHandlePositionRef.current;
}}
>
<div className="h-8 w-2 rounded-full bg-yellow-500" />
</div>
<div
ref={rightHandleRef}
className="bg-background absolute z-20 flex h-full w-7 cursor-ew-resize touch-none items-center justify-center rounded-full border border-yellow-500 select-none"
style={{
left: hasMeasuredWidth ? "-30px" : undefined,
right: hasMeasuredWidth ? undefined : "0px",
translate: hasMeasuredWidth ? `${rightHandle}px 0` : undefined,
}}
onPointerDown={(event) => {
event.preventDefault();
rightHandleRef.current?.setPointerCapture(event.pointerId);
dragRef.current.isDragging = true;
dragRef.current.side = "right";
dragRef.current.pointerId = event.pointerId;
dragRef.current.startX = event.clientX;
dragRef.current.initialPosition = rightHandlePositionRef.current;
}}
>
<div className="h-8 w-2 rounded-full bg-yellow-500" />
</div>
</div>
<span
className="relative z-0 inline-flex size-full items-center justify-center rounded-2xl px-9 will-change-auto"
style={{
mask: textMask,
WebkitMask: textMask,
}}
>
{children}
</span>
</div>
</div>
);
}