summaryrefslogtreecommitdiff
path: root/apps/web/src/components/landing
diff options
context:
space:
mode:
authorMaze <167211895+mazeincoding@users.noreply.github.com>2026-01-31 00:20:04 +0100
committerGitHub <noreply@github.com>2026-01-31 00:20:04 +0100
commit7bf0984698e43b9aa2dd879f65951318ed410390 (patch)
treeccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/components/landing
parent0173db99448d1c4c6415dcf15272e17551b48b04 (diff)
downloadopencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/components/landing')
-rw-r--r--apps/web/src/components/landing/handlebars.tsx238
-rw-r--r--apps/web/src/components/landing/hero.tsx103
2 files changed, 187 insertions, 154 deletions
diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx
index 758717e5..da0d895d 100644
--- a/apps/web/src/components/landing/handlebars.tsx
+++ b/apps/web/src/components/landing/handlebars.tsx
@@ -1,105 +1,173 @@
"use client";
-import {
- motion,
- useMotionTemplate,
- useMotionValue,
- useMotionValueEvent,
- useTransform,
-} from "motion/react";
import { type PropsWithChildren, useEffect, useRef, useState } from "react";
type HandlebarsProps = PropsWithChildren;
export function Handlebars({ children }: HandlebarsProps) {
- const containerRef = useRef<HTMLDivElement>(null);
+ const containerRef = useRef<HTMLDivElement>(null);
+ const leftHandleRef = useRef<HTMLDivElement>(null);
+ const rightHandleRef = useRef<HTMLDivElement>(null);
- const [width, setWidth] = useState(0);
+ const [width, setWidth] = useState(0);
+ const [leftHandle, setLeftHandle] = useState(0);
+ const [rightHandle, setRightHandle] = useState(0);
- const [leftHandle, setLeftHandle] = useState(0);
- const [rightHandle, setRightHandle] = useState(width);
+ const widthRef = useRef(0);
+ const leftHandlePositionRef = useRef(0);
+ const rightHandlePositionRef = useRef(0);
- const leftHandleX = useMotionValue(0);
- const rightHandleX = useMotionValue(width);
+ 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,
+ });
- useEffect(() => {
- const el = containerRef.current;
- if (!el) return;
+ useEffect(() => {
+ const el = containerRef.current;
+ if (!el) return;
- const observer = new ResizeObserver(() => {
- setWidth(el.offsetWidth);
- setRightHandle(el.offsetWidth);
- rightHandleX.set(el.offsetWidth);
- });
+ const updateWidth = () => {
+ const newWidth = el.offsetWidth;
+ setWidth(newWidth);
+ setRightHandle(newWidth);
+ };
- observer.observe(el);
- return () => observer.disconnect();
- }, [rightHandleX.set]);
+ const observer = new ResizeObserver(updateWidth);
+ observer.observe(el);
+ updateWidth();
- useMotionValueEvent(leftHandleX, "change", () => {
- setLeftHandle(leftHandleX.get());
- });
- useMotionValueEvent(rightHandleX, "change", () => {
- setRightHandle(rightHandleX.get());
- });
- const leftGradient = useTransform(leftHandleX, [0, width - 10], [0, 100]);
- const rightGradient = useTransform(rightHandleX, [0, width + 10], [0, 100]);
+ return () => observer.disconnect();
+ }, []);
- return (
- <div className="flex justify-center gap-4 leading-16">
- <div ref={containerRef} className="relative -rotate-[2.76deg] mt-0.5">
- <div className="absolute inset-0 w-full h-full rounded-2xl border border-yellow-500 flex justify-between z-1">
- <motion.div
- className="absolute z-10 left-0 h-full border border-yellow-500 w-7 rounded-full bg-background flex items-center justify-center select-none"
- style={{
- x: leftHandleX,
- }}
- drag="x"
- dragConstraints={{ left: 0, right: rightHandle - 60 }}
- dragElastic={0}
- dragMomentum={false}
- whileHover={{ scale: 1.05, cursor: "grab" }}
- whileDrag={{ scale: 1.1, cursor: "grabbing" }}
- transition={{ type: "spring", stiffness: 400, damping: 30 }}
- >
- <div className="w-2 h-8 rounded-full bg-yellow-500" />
- </motion.div>
+ useEffect(() => {
+ widthRef.current = width;
+ leftHandlePositionRef.current = leftHandle;
+ rightHandlePositionRef.current = rightHandle;
+ }, [leftHandle, rightHandle, width]);
- <motion.div
- className="absolute z-10 -left-[30px] h-full border border-yellow-500 w-7 rounded-full bg-background flex items-center justify-center select-none"
- style={{
- x: rightHandleX,
- }}
- drag="x"
- dragConstraints={{
- left: leftHandle + 60,
- right: width,
- }}
- dragElastic={0}
- dragMomentum={false}
- whileHover={{ scale: 1.05, cursor: "grab" }}
- whileDrag={{ scale: 1.1, cursor: "grabbing" }}
- transition={{ type: "spring", stiffness: 400, damping: 30 }}
- >
- <div className="w-2 h-8 rounded-full bg-yellow-500" />
- </motion.div>
- </div>
+ useEffect(() => {
+ const handlePointerMove = (event: PointerEvent) => {
+ const { isDragging, side, pointerId, startX, initialPosition } =
+ dragRef.current;
- <motion.span
- className="inline-flex items-center justify-center w-full h-full px-9 relative rounded-2xl will-change-auto"
- style={{
- mask: useMotionTemplate`linear-gradient(90deg,
+ 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 leftGradientPercent = width > 0 ? (leftHandle / (width - 10)) * 100 : 0;
+ const rightGradientPercent =
+ width > 0 ? (rightHandle / (width + 10)) * 100 : 0;
+
+ 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 -left-[30px] 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: `${rightHandle}px 0`,
+ }}
+ 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: `linear-gradient(90deg,
rgba(255, 255, 255, 0) 0%,
- rgba(255, 255, 255, 0) ${leftGradient}%,
- rgba(0, 0, 0) ${leftGradient}%,
- rgba(0, 0, 0) ${rightGradient}%,
- rgba(255, 255, 255, 0) ${rightGradient}%,
+ 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%)`,
- }}
- >
- {children}
- </motion.span>
- </div>
- </div>
- );
+ }}
+ >
+ {children}
+ </span>
+ </div>
+ </div>
+ );
}
diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx
index 1b557412..6f11ad7c 100644
--- a/apps/web/src/components/landing/hero.tsx
+++ b/apps/web/src/components/landing/hero.tsx
@@ -1,81 +1,46 @@
"use client";
-import { motion } from "motion/react";
import { Button } from "../ui/button";
-import { SponsorButton } from "../ui/sponsor-button";
-import { VercelIcon } from "../icons";
import { ArrowRight } from "lucide-react";
-
import Image from "next/image";
import { Handlebars } from "./handlebars";
import Link from "next/link";
export function Hero() {
- return (
- <div className="min-h-[calc(100svh-4.5rem)] flex flex-col justify-between items-center text-center px-4">
- <Image
- className="absolute top-0 left-0 -z-50 size-full object-cover invert dark:invert-0 opacity-85"
- src="/landing-page-dark.png"
- height={1903.5}
- width={1269}
- alt="landing-page.bg"
- />
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ duration: 1 }}
- className="max-w-3xl mx-auto w-full flex-1 flex flex-col justify-center"
- >
- <motion.div
- initial={{ opacity: 0, y: 20 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ delay: 0.6, duration: 0.8 }}
- className="mb-4 flex justify-center"
- >
- <SponsorButton
- href="https://vercel.com/home?utm_source=opencut"
- logo={VercelIcon}
- companyName="Vercel"
- />
- </motion.div>
- <motion.div
- initial={{ opacity: 0, y: 20 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ delay: 0.2, duration: 0.8 }}
- className="inline-block font-bold tracking-tighter text-4xl md:text-[4rem]"
- >
- <h1>The Open Source</h1>
- <Handlebars>Video Editor</Handlebars>
- </motion.div>
+ return (
+ <div className="flex min-h-[calc(100svh-4.5rem)] flex-col items-center justify-between px-4 text-center">
+ <Image
+ className="absolute top-0 left-0 -z-50 size-full object-cover opacity-85 invert dark:invert-0"
+ src="/landing-page-dark.png"
+ height={1903.5}
+ width={1269}
+ alt="landing-page.bg"
+ />
+ <div className="mx-auto flex w-full max-w-3xl flex-1 flex-col justify-center">
+ <div className="inline-block text-4xl font-bold tracking-tighter md:text-[4rem]">
+ <h1>The open source</h1>
+ <Handlebars>Video editor</Handlebars>
+ </div>
- <motion.p
- className="mt-10 text-base sm:text-xl text-muted-foreground font-light tracking-wide max-w-xl mx-auto"
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ delay: 0.4, duration: 0.8 }}
- >
- A simple but powerful video editor that gets the job done. Works on
- any platform.
- </motion.p>
+ <p className="text-muted-foreground mx-auto mt-10 max-w-xl text-base font-light tracking-wide sm:text-xl">
+ A simple but powerful video editor that gets the job done. Works on
+ any platform.
+ </p>
- <motion.div
- className="mt-8 flex gap-8 justify-center"
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ delay: 0.6, duration: 0.8 }}
- >
- <Link href="/projects">
- <Button
- type="submit"
- size="lg"
- className="px-6 h-11 text-base bg-foreground"
- >
- Try early beta
- <ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" />
- </Button>
- </Link>
- </motion.div>
- </motion.div>
- </div>
- );
+ <div className="mt-8 flex justify-center gap-8">
+ <Link href="/projects">
+ <Button
+ variant="foreground"
+ type="submit"
+ size="lg"
+ className="h-11 text-base"
+ >
+ Try early beta
+ <ArrowRight className="ml-0.5" />
+ </Button>
+ </Link>
+ </div>
+ </div>
+ </div>
+ );
}