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

205 lines
5.9 KiB
TypeScript
Raw Normal View History

2025-07-09 17:30:37 +05:30
"use client";
2025-07-12 16:50:34 +02:00
2025-07-09 17:30:37 +05:30
import React, { useState, useRef, useEffect } from "react";
import { motion, useMotionValue, useTransform, PanInfo } from "motion/react";
interface HandlebarsProps {
children: React.ReactNode;
minWidth?: number;
maxWidth?: number;
onRangeChange?: (left: number, right: number) => void;
}
2025-07-12 16:50:34 +02:00
export function Handlebars({
2025-07-09 17:30:37 +05:30
children,
minWidth = 50,
maxWidth = 400,
onRangeChange,
2025-07-12 16:50:34 +02:00
}: HandlebarsProps) {
2025-07-09 17:30:37 +05:30
const [leftHandle, setLeftHandle] = useState(0);
const [rightHandle, setRightHandle] = useState(maxWidth);
const [contentWidth, setContentWidth] = useState(maxWidth);
2025-07-16 17:36:47 +02:00
const [isDragging, setIsDragging] = useState(false);
2025-07-09 17:30:37 +05:30
const leftHandleX = useMotionValue(0);
const rightHandleX = useMotionValue(maxWidth);
const visibleWidth = useTransform(
[leftHandleX, rightHandleX],
(values: number[]) => values[1] - values[0]
);
const contentLeft = useTransform(leftHandleX, (left: number) => -left);
const containerRef = useRef<HTMLDivElement>(null);
const measureRef = useRef<HTMLDivElement>(null);
2025-07-16 17:36:47 +02:00
// Prevent scroll when dragging on mobile
useEffect(() => {
const preventDefault = (e: TouchEvent) => {
if (isDragging) {
e.preventDefault();
}
};
if (isDragging) {
document.addEventListener("touchmove", preventDefault, {
passive: false,
});
document.body.style.overflow = "hidden";
}
return () => {
document.removeEventListener("touchmove", preventDefault);
document.body.style.overflow = "";
};
}, [isDragging]);
2025-07-09 17:30:37 +05:30
useEffect(() => {
if (!measureRef.current) return;
const measureContent = () => {
if (measureRef.current) {
const width = measureRef.current.scrollWidth;
const paddedWidth = width + 32;
setContentWidth(paddedWidth);
setRightHandle(paddedWidth);
rightHandleX.set(paddedWidth);
}
};
measureContent();
const timer = setTimeout(measureContent, 50);
return () => clearTimeout(timer);
}, [children, rightHandleX]);
useEffect(() => {
leftHandleX.set(leftHandle);
}, [leftHandle, leftHandleX]);
useEffect(() => {
rightHandleX.set(rightHandle);
}, [rightHandle, rightHandleX]);
useEffect(() => {
onRangeChange?.(leftHandle, rightHandle);
}, [leftHandle, rightHandle, onRangeChange]);
const handleLeftDrag = (event: any, info: PanInfo) => {
const newLeft = Math.max(
0,
Math.min(leftHandle + info.offset.x, rightHandle - minWidth)
);
setLeftHandle(newLeft);
};
const handleRightDrag = (event: any, info: PanInfo) => {
const newRight = Math.max(
leftHandle + minWidth,
Math.min(contentWidth, rightHandle + info.offset.x)
);
setRightHandle(newRight);
};
2025-07-16 17:36:47 +02:00
const handleDragStart = () => {
setIsDragging(true);
};
const handleDragEnd = () => {
setIsDragging(false);
};
2025-07-09 17:30:37 +05:30
return (
<div className="flex justify-center gap-4 leading-[4rem] mt-0 md:mt-2">
<div
ref={measureRef}
className="absolute -left-[9999px] top-0 invisible px-4 whitespace-nowrap font-[inherit]"
>
{children}
</div>
<div
ref={containerRef}
className="relative -rotate-[2.76deg] max-w-[250px] md:max-w-[454px] mt-2"
style={{ width: contentWidth }}
>
2025-07-12 16:50:34 +02:00
<div className="absolute inset-0 w-full h-full rounded-2xl border border-yellow-500 flex justify-between">
2025-07-16 17:36:47 +02:00
{/* Left Handle */}
2025-07-09 17:30:37 +05:30
<motion.div
2025-07-16 17:36:47 +02:00
className="h-full border border-yellow-500 rounded-full bg-accent flex items-center justify-center cursor-ew-resize select-none touch-none
w-9 md:w-7 min-h-[44px] md:min-h-0"
2025-07-09 17:30:37 +05:30
style={{
position: "absolute",
x: leftHandleX,
left: 0,
zIndex: 10,
}}
drag="x"
dragConstraints={{ left: 0, right: rightHandle - minWidth }}
dragElastic={0}
dragMomentum={false}
onDrag={handleLeftDrag}
2025-07-16 17:36:47 +02:00
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
2025-07-09 17:30:37 +05:30
whileHover={{ scale: 1.05 }}
whileDrag={{ scale: 1.1 }}
2025-07-16 17:36:47 +02:00
whileTap={{ scale: 1.1 }}
2025-07-09 17:30:37 +05:30
transition={{ type: "spring", stiffness: 400, damping: 30 }}
>
2025-07-12 16:50:34 +02:00
<div className="w-2 h-8 rounded-full bg-yellow-500"></div>
2025-07-09 17:30:37 +05:30
</motion.div>
2025-07-16 17:36:47 +02:00
{/* Right Handle */}
2025-07-09 17:30:37 +05:30
<motion.div
2025-07-16 17:36:47 +02:00
className="h-full border border-yellow-500 rounded-full bg-accent flex items-center justify-center cursor-ew-resize select-none touch-none
w-9 md:w-7 min-h-[44px] md:min-h-0"
2025-07-09 17:30:37 +05:30
style={{
position: "absolute",
x: rightHandleX,
2025-07-16 17:36:47 +02:00
left: -36, // Adjusted for larger mobile handle
2025-07-09 17:30:37 +05:30
zIndex: 10,
}}
drag="x"
dragConstraints={{
left: leftHandle + minWidth,
right: contentWidth,
}}
dragElastic={0}
dragMomentum={false}
onDrag={handleRightDrag}
2025-07-16 17:36:47 +02:00
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
2025-07-09 17:30:37 +05:30
whileHover={{ scale: 1.05 }}
whileDrag={{ scale: 1.1 }}
2025-07-16 17:36:47 +02:00
whileTap={{ scale: 1.1 }}
2025-07-09 17:30:37 +05:30
transition={{ type: "spring", stiffness: 400, damping: 30 }}
>
2025-07-12 16:50:34 +02:00
<div className="w-2 h-8 rounded-full bg-yellow-500"></div>
2025-07-09 17:30:37 +05:30
</motion.div>
</div>
<motion.div
className="relative overflow-hidden rounded-2xl"
style={{
width: visibleWidth,
x: leftHandleX,
height: "100%",
}}
>
<motion.div
className="w-full h-full flex items-center justify-center px-4"
style={{
x: contentLeft,
width: contentWidth,
whiteSpace: "nowrap",
}}
>
{children}
</motion.div>
</motion.div>
</div>
</div>
);
2025-07-16 17:36:47 +02:00
}