summaryrefslogtreecommitdiff
path: root/apps/web
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/src/components/landing/handlebars.tsx164
-rw-r--r--apps/web/src/components/landing/hero.tsx10
2 files changed, 166 insertions, 8 deletions
diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx
new file mode 100644
index 00000000..36705e90
--- /dev/null
+++ b/apps/web/src/components/landing/handlebars.tsx
@@ -0,0 +1,164 @@
+"use client";
+
+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;
+}
+
+export function Handlebars({
+ children,
+ minWidth = 50,
+ maxWidth = 400,
+ onRangeChange,
+}: HandlebarsProps) {
+ const [leftHandle, setLeftHandle] = useState(0);
+ const [rightHandle, setRightHandle] = useState(maxWidth);
+ const [contentWidth, setContentWidth] = useState(maxWidth);
+
+ 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);
+
+ 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);
+ };
+
+ 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 }}
+ >
+ <div className="absolute inset-0 w-full h-full rounded-2xl border border-yellow-500 flex justify-between">
+ <motion.div
+ className="h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center cursor-ew-resize select-none"
+ style={{
+ position: "absolute",
+ x: leftHandleX,
+ left: 0,
+ zIndex: 10,
+ }}
+ drag="x"
+ dragConstraints={{ left: 0, right: rightHandle - minWidth }}
+ dragElastic={0}
+ dragMomentum={false}
+ onDrag={handleLeftDrag}
+ whileHover={{ scale: 1.05 }}
+ whileDrag={{ scale: 1.1 }}
+ transition={{ type: "spring", stiffness: 400, damping: 30 }}
+ >
+ <div className="w-2 h-8 rounded-full bg-yellow-500"></div>
+ </motion.div>
+
+ <motion.div
+ className="h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center cursor-ew-resize select-none"
+ style={{
+ position: "absolute",
+ x: rightHandleX,
+ left: -30,
+ zIndex: 10,
+ }}
+ drag="x"
+ dragConstraints={{
+ left: leftHandle + minWidth,
+ right: contentWidth,
+ }}
+ dragElastic={0}
+ dragMomentum={false}
+ onDrag={handleRightDrag}
+ whileHover={{ scale: 1.05 }}
+ whileDrag={{ scale: 1.1 }}
+ transition={{ type: "spring", stiffness: 400, damping: 30 }}
+ >
+ <div className="w-2 h-8 rounded-full bg-yellow-500"></div>
+ </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>
+ );
+};
diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx
index 65fbb3de..776cc654 100644
--- a/apps/web/src/components/landing/hero.tsx
+++ b/apps/web/src/components/landing/hero.tsx
@@ -8,6 +8,7 @@ import { useState } from "react";
import { useToast } from "@/hooks/use-toast";
import Image from "next/image";
+import { Handlebars } from "./handlebars";
interface HeroProps {
signupCount: number;
@@ -91,14 +92,7 @@ export function Hero({ signupCount }: HeroProps) {
className="inline-block font-bold tracking-tighter text-4xl md:text-[4rem]"
>
<h1>The Open Source</h1>
- <div className="flex justify-center gap-4 leading-[4rem] mt-0 md:mt-2">
- <div className="relative -rotate-[2.76deg] max-w-[250px] md:max-w-[454px] mt-2">
- <Image src="/frame.svg" height={79} width={459} alt="frame" />
- <span className="absolute inset-0 flex items-center justify-center">
- Video Editor
- </span>
- </div>
- </div>
+ <Handlebars>Video Editor</Handlebars>
</motion.div>
<motion.p