'use client'; import Image from 'next/image'; import Link from 'next/link'; import { useEffect, useRef } from 'react'; import { gsap } from 'gsap'; import { CheckSquare, Clock3, FileVideo, GitBranch, Github, GitMerge, Grid3X3, Layers3, MessageSquare, MessageSquareDot, Mic, MoveRight, PenTool, Play, Upload, Video, Waves, } from 'lucide-react'; interface LandingPageProps { isLoggedIn: boolean; } const controlButtonClass = 'group relative isolate inline-flex h-8 items-center justify-center overflow-hidden border border-border bg-background px-2.5 text-[11px] font-medium text-foreground transition-colors duration-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring sm:h-9 sm:px-4 sm:text-xs'; const primaryButtonClass = 'group relative isolate inline-flex h-10 items-center justify-center overflow-hidden border border-primary bg-primary px-4 text-[11px] font-medium text-primary-foreground transition-transform duration-300 hover:scale-[1.02] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring sm:h-11 sm:px-5 sm:text-xs'; const secondaryButtonClass = 'group relative isolate inline-flex h-10 items-center justify-center overflow-hidden border border-border bg-background px-4 text-[11px] font-medium text-foreground transition-colors duration-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring sm:h-11 sm:px-5 sm:text-xs'; const workflowProofItems = [ { step: '01', title: 'Upload Or Import', description: 'Drop MP4 files or import YouTube links into one project timeline.', kind: 'upload', }, { step: '02', title: 'Comment At Timestamp', description: 'Text and voice notes are attached to exact moments on the timeline.', kind: 'comment', }, { step: '03', title: 'Ship Next Version', description: 'Upload a new cut while preserving feedback history and review context.', kind: 'version', }, ] as const; const useCaseRails = [ { title: 'Agency Teams', label: 'CLIENT REVIEW', description: 'Share guest links and collect timestamped approvals without account friction.', image: 'https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1200&q=80', }, { title: 'Solo Editors', label: 'PERSONAL QC', description: 'Freelance editors can share review links with clients and close notes without account friction.', image: 'https://images.unsplash.com/photo-1627244714766-94dab62ed964?q=80&w=765&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', }, { title: 'Internal Teams', label: 'CAMPAIGN OPS', description: 'Coordinate editors, PMs, and stakeholders across one shared workspace.', image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&w=1200&q=80', }, ] as const; function formatTime(totalSeconds: number): string { const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `00:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } export function LandingPage({ isLoggedIn }: LandingPageProps) { const rootRef = useRef(null); const navbarRef = useRef(null); const timelineCounterRef = useRef(null); const heroAnnotationCircleRef = useRef(null); const featureAnnotationCircleRef = useRef(null); useEffect(() => { const cleanupHandlers: Array<() => void> = []; const ctx = gsap.context(() => { gsap.from('[data-hero-copy]', { y: 30, opacity: 0, duration: 0.9, stagger: 0.12, ease: 'power3.out', }); gsap.from('[data-reveal]', { y: 24, opacity: 0, duration: 0.8, stagger: 0.08, ease: 'power3.out', }); gsap.to('.hero-float-card', { y: -14, duration: 2.6, stagger: 0.2, yoyo: true, repeat: -1, ease: 'power3.out', }); const timelineCounter = timelineCounterRef.current; if (timelineCounter) { const playhead = { seconds: 83 }; gsap.to(playhead, { seconds: 224, duration: 12, repeat: -1, ease: 'none', onUpdate: () => { timelineCounter.textContent = formatTime(Math.floor(playhead.seconds)); }, }); } const timelineDrops = gsap.utils.toArray('.timeline-drop'); timelineDrops.forEach((drop, index) => { const timeline = gsap.timeline({ repeat: -1, delay: index * 0.35 }); timeline .fromTo( drop, { y: -18, opacity: 0 }, { y: 0, opacity: 1, duration: 0.8, ease: 'power3.out' } ) .to(drop, { opacity: 0, duration: 0.35, ease: 'power3.out' }, '+=0.5'); }); const waveformBars = gsap.utils.toArray('.voice-bar'); waveformBars.forEach((bar, index) => { gsap.set(bar, { transformOrigin: 'center bottom' }); gsap.to(bar, { scaleY: gsap.utils.random(0.35, 1.45), duration: gsap.utils.random(0.45, 0.9), repeat: -1, yoyo: true, delay: index * 0.03, ease: 'power3.out', }); }); gsap.fromTo( '.voice-scan', { width: '0%' }, { width: '100%', duration: 2.4, repeat: -1, yoyo: true, ease: 'power3.out', } ); const animateStroke = (shape: SVGCircleElement | null, duration: number) => { if (!shape) return; const shapeLength = shape.getTotalLength(); gsap.set(shape, { strokeDasharray: shapeLength, strokeDashoffset: shapeLength, }); gsap.to(shape, { strokeDashoffset: 0, duration, repeat: -1, yoyo: true, repeatDelay: 0.6, ease: 'power3.out', }); }; animateStroke(heroAnnotationCircleRef.current, 2.4); animateStroke(featureAnnotationCircleRef.current, 2.1); const nav = navbarRef.current; if (nav) { const updateNavbar = () => { const hasScrolled = window.scrollY > 12; gsap.to(nav, { backgroundColor: hasScrolled ? 'color-mix(in oklab, var(--background) 80%, transparent)' : 'transparent', backdropFilter: hasScrolled ? 'blur(12px)' : 'blur(0px)', duration: 0.3, overwrite: 'auto', ease: 'power3.out', }); }; updateNavbar(); window.addEventListener('scroll', updateNavbar, { passive: true }); cleanupHandlers.push(() => window.removeEventListener('scroll', updateNavbar)); } const magneticItems = gsap.utils.toArray('[data-magnetic]'); magneticItems.forEach((item) => { const xTo = gsap.quickTo(item, 'x', { duration: 0.35, ease: 'power3.out' }); const yTo = gsap.quickTo(item, 'y', { duration: 0.35, ease: 'power3.out' }); const onMove = (event: MouseEvent) => { const bounds = item.getBoundingClientRect(); const x = event.clientX - (bounds.left + bounds.width / 2); const y = event.clientY - (bounds.top + bounds.height / 2); xTo((x / bounds.width) * 10); yTo((y / bounds.height) * 10); }; const onLeave = () => { xTo(0); yTo(0); }; item.addEventListener('mousemove', onMove); item.addEventListener('mouseleave', onLeave); cleanupHandlers.push(() => { item.removeEventListener('mousemove', onMove); item.removeEventListener('mouseleave', onLeave); }); }); }, rootRef); return () => { cleanupHandlers.forEach((cleanup) => cleanup()); ctx.revert(); }; }, []); return (

OPEN SOURCE VIDEO REVIEW TOOL

Review Video. Without the Chaos.

Timestamped text, voice feedback, and version control for modern creative teams.

Start using OpenFrame GitHub Repo
PLAYHEAD 00:02:14
v2.3.1
Annotated video frame preview

ANNOTATION

Focus this subject isolation for scene 04.

00:01:23

Re-time this beat hit by 4 frames.

Voice Note

Transition pacing reads cleaner in this cut.

Fast, precise collaborative video review.

Guest commenting via shareable links and workspace-level permissions are available on every project.

The Timeline

00:01:23

Comments attach to exact timestamps so every request references the exact moment on the video.

SEQUENCE.A / MASTER CUT 24 FPS
00:00:23
00:00:52
00:01:34

Voice Notes

Record a voice note at any timestamp. Teammates play it back in context.

REC 00:00:12
{Array.from({ length: 22 }).map((_, index) => ( ))}

MIC-01 / SAVED TO 00:01:23

Version Management

Upload multiple cuts under one video and keep each version named and ordered.

v1.0.0 - rough-cut Archived
v1.0.1 - client-notes Previous
v1.0.2 - current Active
Upload New Version

Drawing & Annotation

Draw directly on the video image to point at exact areas during review.

Video frame with annotation overlay
DRAW ON FRAME / SAVE WITH COMMENT

Upload. Review. Version. In one continuous loop.

{workflowProofItems.map((item) => (
{item.kind === 'upload' && (

Add Source

Paste URL Direct Upload
https://youtube.com/watch?v=...
Video title (optional)
Description (optional)
Add Video
)} {item.kind === 'comment' && (
Y Yusuf İpek
00:04:14

wrong tweet here's what you should use:

https://x.com/...

2/21/2026 Feedback
↩ Reply
)} {item.kind === 'version' && (

New Version

v1.0.3

Link URL Upload File
Video URL
Version Label
Ship Version
)}

{item.step}

{item.title}

{item.description}

))}

One screen. Full review context.

This panel maps exactly where timestamp comments, voice notes, and frame annotation live.

OpenFrame dashboard deep dive screenshot

Timestamp Comments

Attached to exact timeline positions.

Voice Notes

Captured in context, played back in thread.

Frame Annotation

Circle and draw directly on the frame.

Sync 2-4 videos and compare cuts in real time.

Play multiple versions side-by-side with synchronized playback to validate pacing, edit decisions, and grading changes.

OpenFrame compare mode screenshot

2-4 Video Panels

Load multiple versions into one comparison view.

Synchronized Playback

Play, pause, and scrub all panels at once.

Compare Feedback Impact

Confirm that requested changes are visible across versions.

Built for agencies, solo editors, and internal teams.

{useCaseRails.map((item) => (
{`${item.title}

{item.label}

{item.title}

{item.description}

))}

Deploy self-hosted, or run fully managed.

For the builders.

Open Source (Self-Hosted)

Free / OSS

  • Full codebase access
  • Self-hosted infrastructure control
  • Manual update cadence
View GitHub

For modern creative teams.

Hosted Cloud

$10 / month

  • Unlimited Seats (Collaborators/Guests)
  • Unlimited Workspaces & Projects
  • Unlimited YouTube video imports
  • 100 GB of direct video upload storage (+$5 per next 100 GB)
  • Download uploaded original files as-is
  • Instant Webhook (Telegram/Email) Setup
{isLoggedIn ? 'Go to Dashboard' : 'Start Now'}
); }