### Stack
- **Vite** + **React 18** + **TypeScript**
- **Tailwind CSS 3.4**
- **lucide-react** for icons (`LogIn`, `UserPlus`, `Play`, `Sparkles`, `Menu`, `X`)
- No Framer Motion -- all animations are CSS `transition-*` classes
---
### Fonts (loaded in `index.html`)
```html
```
Body/root font stack (in `index.css`):
```css
html, body, #root {
height: 100%;
margin: 0;
font-family: 'Neue Haas Grotesk Display Pro 55 Roman', 'Neue Haas Grotesk Text Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
```
---
### Video URL (CloudFront)
```
https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260511_131941_d136af49-e243-493a-be14-6ff3f24e09e6.mp4
```
---
### Color Palette
| Token | Hex |
|-------|-----|
| Dark green (text, buttons) | `#1f2a1d` |
| Medium dark green | `#2d3a2a` |
| Button hover | `#2a3827` |
| Body text green | `#4b5b47` |
| Heading primary | `#336443` |
| Heading accent | `#85AB8B` |
| Bottom-left text | `#3d5638` |
| Bottom-left button bg | `#3d5638`, hover `#2d4228` |
---
### Architecture
Two files:
1. **`BoomerangVideoBg.tsx`** -- captures video frames into canvas, then plays them forward/backward in a seamless boomerang loop at 30fps (960px max capture width).
2. **`App.tsx`** -- the full hero section.
---
### `BoomerangVideoBg.tsx` (exact)
```tsx
import { useEffect, useRef, useState } from 'react';
type Props = {
src: string;
className?: string;
};
export default function BoomerangVideoBg({ src, className }: Props) {
const videoRef = useRef(null);
const displayCanvasRef = useRef(null);
const [framesReady, setFramesReady] = useState(false);
const framesRef = useRef([]);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const frames: HTMLCanvasElement[] = [];
let capturing = true;
let lastTime = -1;
const MAX_WIDTH = 960;
const captureFrame = () => {
if (!capturing || video.readyState < 2) return;
if (video.currentTime === lastTime) return;
lastTime = video.currentTime;
const vw = video.videoWidth;
const vh = video.videoHeight;
if (!vw || !vh) return;
const scale = Math.min(1, MAX_WIDTH / vw);
const w = Math.round(vw * scale);
const h = Math.round(vh * scale);
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.drawImage(video, 0, 0, w, h);
frames.push(canvas);
};
type VFCVideo = HTMLVideoElement & {
requestVideoFrameCallback?: (cb: () => void) => number;
};
const vfcVideo = video as VFCVideo;
const hasVFC = typeof vfcVideo.requestVideoFrameCallback === 'function';
let rafId = 0;
const rafLoop = () => {
captureFrame();
if (capturing) rafId = requestAnimationFrame(rafLoop);
};
const vfcLoop = () => {
captureFrame();
if (capturing && vfcVideo.requestVideoFrameCallback) {
vfcVideo.requestVideoFrameCallback(vfcLoop);
}
};
const onEnded = () => {
capturing = false;
if (frames.length > 0) {
framesRef.current = frames;
setFramesReady(true);
}
};
const onLoaded = () => {
video.play().catch(() => {});
if (hasVFC) {
vfcVideo.requestVideoFrameCallback!(vfcLoop);
} else {
rafId = requestAnimationFrame(rafLoop);
}
};
video.addEventListener('loadedmetadata', onLoaded);
video.addEventListener('ended', onEnded);
if (video.readyState >= 1) onLoaded();
return () => {
capturing = false;
cancelAnimationFrame(rafId);
video.removeEventListener('loadedmetadata', onLoaded);
video.removeEventListener('ended', onEnded);
};
}, [src]);
useEffect(() => {
if (!framesReady) return;
const canvas = displayCanvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const frames = framesRef.current;
if (frames.length === 0) return;
const first = frames[0];
canvas.width = first.width;
canvas.height = first.height;
let index = 0;
let direction = 1;
let last = performance.now();
const interval = 1000 / 30;
let rafId = 0;
const render = (now: number) => {
if (now - last >= interval) {
last = now;
ctx.drawImage(frames[index], 0, 0);
index += direction;
if (index >= frames.length - 1) {
index = frames.length - 1;
direction = -1;
} else if (index cancelAnimationFrame(rafId);
}, [framesReady]);
return (
);
}
```
---
### `App.tsx` (exact)
```tsx
import { useState, useEffect } from 'react';
import { LogIn, UserPlus, Play, Sparkles, Menu, X } from 'lucide-react';
import BoomerangVideoBg from './BoomerangVideoBg';
const BG_VIDEO =
'https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260511_131941_d136af49-e243-493a-be14-6ff3f24e09e6.mp4';
function App() {
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
if (menuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [menuOpen]);
const navLinks = [
{ href: '#mission', label: 'Purpose' },
{ href: '#how', label: 'The Process' },
{ href: '#pricing', label: 'Tariffs' },
];
return (
LinkFlowTM
{navLinks.map((link, i) => (
{link.label}
))}
Try it Live
Sign Me Up!
Enter
setMenuOpen((v) => !v)}
className="lg:hidden relative flex items-center justify-center w-10 h-10 rounded-full bg-white/70 backdrop-blur-md border border-white/60 text-[#1f2a1d] transition-all duration-300 hover:bg-white/90"
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={menuOpen}
>
{/* Mobile menu overlay */}
setMenuOpen(false)}
>
{/* Mobile menu drawer */}
{navLinks.map((link, i) => (
setMenuOpen(false)}
className={`text-2xl font-semibold text-[#1f2a1d] py-4 border-b border-[#1f2a1d]/10 transition-all duration-500 ${
menuOpen ? 'translate-x-0 opacity-100' : 'translate-x-8 opacity-0'
}`}
style={{ transitionDelay: menuOpen ? `${150 + i * 70}ms` : '0ms' }}
>
{link.label}
))}
Sign Me Up!
Enter
Try it Live
{/* Hero copy */}
Close the rift{' '}
linking
signals and action
Shape scattered signals into meaningful outcomes via AI-driven workflows.
{/* Bottom-left CTA block */}
FluxEngineTM
LinkFlow smoothly unites your company systems, streamlining data paths between services without having to write custom scripts.
Try it Live
Know More.
{/* Bottom-right video link */}
How we build?
1:35
);
}
export default App;
```
---
### Animation Details (all CSS, no Framer Motion)
| Element | Property | Values |
|---------|----------|--------|
| Hamburger Menu/X icon swap | `transition-all duration-300` | Open: Menu gets `opacity-0 rotate-90 scale-50`, X gets `opacity-100 rotate-0 scale-100`. Closed: reverse. |
| Mobile overlay backdrop | `transition-opacity duration-300` | Open: `opacity-100 pointer-events-auto`. Closed: `opacity-0 pointer-events-none`. |
| Mobile drawer slide | `transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]` | Open: `translate-x-0`. Closed: `translate-x-full`. |
| Mobile nav links stagger | `transition-all duration-500` | Open: `translate-x-0 opacity-100`, delay per item: `150ms + i * 70ms`. Closed: `translate-x-8 opacity-0`, delay `0ms`. |
| Mobile CTA group | `transition-all duration-500` | Open: `translate-x-0 opacity-100`, delay `400ms`. Closed: `translate-x-8 opacity-0`, delay `0ms`. |
| Nav buttons | `transition-colors` | Default Tailwind duration (150ms). |
| Opacity links | `transition-opacity` | `hover:opacity-80`. |
---
### Key Layout/Spacing Notes
- Root section: `relative w-full min-h-screen sm:h-screen overflow-hidden`
- Navbar padding: `px-4 sm:px-6 md:px-10 py-4 sm:py-6`
- Desktop pill nav: `bg-white/70 backdrop-blur-md rounded-full pl-6 pr-1 py-1 shadow-sm border border-white/60`
- Hero heading: `pt-24 sm:pt-28 md:pt-32`, font sizes `text-[2rem] sm:text-4xl md:text-5xl lg:text-[4.75rem] xl:text-[5.25rem]`, `leading-[0.95]`, `letterSpacing: '-0.035em'`
- Bottom-left block: `absolute left-4 right-4 sm:right-auto sm:left-6 md:left-10 bottom-6 sm:bottom-8 md:bottom-10`
- Bottom-right video: `absolute right-6 md:right-10 bottom-8 md:bottom-10`
---
### Dependencies (package.json)
```json
{
"dependencies": {
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.18",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.5.3",
"vite": "^5.4.2"
}
}
```