Agent Terminal Hero

A terminal types a prompt and streams an agent's plan while the generated dashboard assembles itself beside it.

  • Heroes
  • Developer tools
  • React only · 0 deps
  • 1 file
Full screen

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/hero-ai-terminal.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/heroes/hero-ai-terminal.tsx
1/**
2 * HeroAiTerminal — a dark developer-tools hero for an AI coding agent. A
3 * terminal window types the user's prompt, streams the agent's plan line by
4 * line with tool calls and diffs, and a live preview beside it assembles the
5 * generated UI: cards slide into a grid, a chart draws, a toast lands. The
6 * sequence loops with a short rest. The whole composition tilts slightly to
7 * the pointer and, on scroll, the windows separate in depth while the headline
8 * parallaxes away.
9 *
10 * Props: title, lede, prompt (string typed into the terminal), plan (lines),
11 * primaryCta, secondaryCta, loop (default true).
12 * CSS vars: --fd-agt-bg, --fd-agt-accent, --fd-agt-mono.
13 *
14 * Browser support: all evergreen browsers.
15 * Motion: one rAF scheduler drives typing/streaming (paused off-screen and in
16 * hidden tabs), pointer tilt via spring (k=380, c=30), scroll via rAF +
17 * getBoundingClientRect (transform/opacity only). prefers-reduced-motion
18 * shows the finished state instantly with no loop.
19 * Accessibility: the terminal is an aria-hidden illustration; the prompt and
20 * plan are summarised in a visually hidden paragraph.
21 */
22import { useEffect, useId, useRef, useState } from "react";
23import type { ReactNode } from "react";
24
25export type AgentLine =
26 | { kind: "think"; text: string }
27 | { kind: "tool"; name: string; arg: string; meta?: string }
28 | { kind: "diff"; file: string; add: number; del: number }
29 | { kind: "ok"; text: string };
30
31export interface HeroAiTerminalProps {
32 eyebrow?: ReactNode;
33 title?: ReactNode;
34 lede?: ReactNode;
35 prompt?: string;
36 plan?: AgentLine[];
37 primaryCta?: { label: string; href: string };
38 secondaryCta?: { label: string; href: string };
39 loop?: boolean;
40}
41
42const DEFAULT_PLAN: AgentLine[] = [
43 { kind: "think", text: "Reading schema for `invoices` and `customers`…" },
44 { kind: "tool", name: "read", arg: "db/schema.prisma", meta: "142 lines" },
45 { kind: "tool", name: "grep", arg: "\"useMetrics\" src/", meta: "3 hits" },
46 { kind: "think", text: "Plan: KPI row, MRR chart, churn list, live toast." },
47 { kind: "diff", file: "app/billing/page.tsx", add: 118, del: 4 },
48 { kind: "diff", file: "components/kpi-card.tsx", add: 46, del: 0 },
49 { kind: "tool", name: "test", arg: "billing.spec.ts", meta: "12 passed" },
50 { kind: "ok", text: "Preview ready · 2.8s" },
51];
52
53const PROMPT = "build a billing dashboard: MRR, churn and failed payments — use our design tokens";
54
55// timeline (ms)
56const T_TYPE_START = 500;
57const T_CHAR = 26;
58const T_LINE = 380;
59
60export function HeroAiTerminal({
61 eyebrow = "Fathom Agent 3 — now with background tasks",
62 title = (
63 <>
64 Describe it.
65 <br />
66 <span className="fd-agt-dim">Review it.</span> Ship it.
67 </>
68 ),
69 lede = "An autonomous engineer that reads your codebase, writes the change, runs the tests and opens the pull request — while you stay in flow.",
70 prompt = PROMPT,
71 plan = DEFAULT_PLAN,
72 primaryCta = { label: "Install for VS Code", href: "#install" },
73 secondaryCta = { label: "npx fathom init", href: "#cli" },
74 loop = true,
75}: HeroAiTerminalProps) {
76 const root = useRef<HTMLElement>(null);
77 const scene = useRef<HTMLDivElement>(null);
78 const term = useRef<HTMLDivElement>(null);
79 const prev = useRef<HTMLDivElement>(null);
80 const copyRef = useRef<HTMLDivElement>(null);
81 const [typed, setTyped] = useState(0);
82 const [lines, setLines] = useState(0);
83 const [build, setBuild] = useState(0); // 0..6 preview pieces
84 const [copied, setCopied] = useState(false);
85 const gid = `fd-agt-g${useId().replace(/[^a-zA-Z0-9]/g, "")}`;
86
87 // sequencing
88 useEffect(() => {
89 const el = root.current;
90 if (!el) return;
91 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
92 setTyped(prompt.length);
93 setLines(plan.length);
94 setBuild(6);
95 return;
96 }
97 const typeEnd = T_TYPE_START + prompt.length * T_CHAR;
98 const planStart = typeEnd + 420;
99 const planEnd = planStart + plan.length * T_LINE;
100 const total = planEnd + 5200;
101 let clock = 0;
102 let last = 0;
103 let raf = 0;
104 let visible = true;
105 const tick = (now: number) => {
106 raf = 0;
107 const dt = Math.min(now - last, 64);
108 last = now;
109 clock += dt;
110 let t = clock;
111 if (loop && t > total) {
112 clock = 0;
113 t = 0;
114 }
115 setTyped(Math.max(0, Math.min(prompt.length, Math.floor((t - T_TYPE_START) / T_CHAR))));
116 setLines(Math.max(0, Math.min(plan.length, Math.floor((t - planStart) / T_LINE) + 1)));
117 // preview builds in step with the diff lines
118 const b = t < planStart + 3 * T_LINE ? 0 : Math.min(6, Math.floor((t - (planStart + 3 * T_LINE)) / 300) + 1);
119 setBuild(b);
120 if (visible && !document.hidden && (loop || t < total)) raf = requestAnimationFrame(tick);
121 };
122 const start = () => {
123 if (!raf && visible && !document.hidden) {
124 last = performance.now();
125 raf = requestAnimationFrame(tick);
126 }
127 };
128 const io = new IntersectionObserver(([e]) => {
129 visible = e.isIntersecting;
130 if (visible) start();
131 });
132 io.observe(el);
133 document.addEventListener("visibilitychange", start);
134 start();
135 return () => {
136 cancelAnimationFrame(raf);
137 io.disconnect();
138 document.removeEventListener("visibilitychange", start);
139 };
140 }, [prompt, plan, loop]);
141
142 // pointer tilt + scroll depth
143 useEffect(() => {
144 const el = root.current;
145 if (!el || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
146 const sp = { x: 0, y: 0, vx: 0, vy: 0, tx: 0, ty: 0 };
147 const onMove = (e: PointerEvent) => {
148 if (e.pointerType === "touch") return;
149 const r = el.getBoundingClientRect();
150 sp.tx = ((e.clientX - r.left) / r.width) * 2 - 1;
151 sp.ty = ((e.clientY - r.top) / r.height) * 2 - 1;
152 };
153 const onLeave = () => {
154 sp.tx = 0;
155 sp.ty = 0;
156 };
157 el.addEventListener("pointermove", onMove);
158 el.addEventListener("pointerleave", onLeave);
159 let raf = 0;
160 let last = performance.now();
161 let visible = true;
162 const frame = (now: number) => {
163 raf = 0;
164 const dt = Math.min((now - last) / 1000, 0.032);
165 last = now;
166 for (let i = 0; i < 2; i++) {
167 const h = dt / 2;
168 sp.vx += (-380 * (sp.x - sp.tx) - 30 * sp.vx) * h;
169 sp.vy += (-380 * (sp.y - sp.ty) - 30 * sp.vy) * h;
170 sp.x += sp.vx * h;
171 sp.y += sp.vy * h;
172 }
173 const r = el.getBoundingClientRect();
174 const p = Math.min(1, Math.max(0, -r.top / Math.max(1, r.height)));
175 if (scene.current) scene.current.style.transform = `rotateX(${(6 - sp.y * 4 + p * 14).toFixed(2)}deg) rotateY(${(sp.x * 6).toFixed(2)}deg) translate3d(0, ${(p * -40).toFixed(1)}px, 0)`;
176 if (term.current) term.current.style.transform = `translate3d(${(p * -60).toFixed(1)}px, 0, ${(30 + p * 60).toFixed(1)}px)`;
177 if (prev.current) prev.current.style.transform = `translate3d(${(p * 80).toFixed(1)}px, ${(p * -30).toFixed(1)}px, ${(-20 - p * 120).toFixed(1)}px)`;
178 if (copyRef.current) {
179 copyRef.current.style.transform = `translate3d(0, ${(p * 120).toFixed(1)}px, 0)`;
180 copyRef.current.style.opacity = `${(1 - p * 1.6).toFixed(3)}`;
181 }
182 if (visible && !document.hidden) raf = requestAnimationFrame(frame);
183 };
184 const start = () => {
185 if (!raf && visible && !document.hidden) {
186 last = performance.now();
187 raf = requestAnimationFrame(frame);
188 }
189 };
190 const io = new IntersectionObserver(([e]) => {
191 visible = e.isIntersecting;
192 if (visible) start();
193 });
194 io.observe(el);
195 document.addEventListener("visibilitychange", start);
196 start();
197 return () => {
198 cancelAnimationFrame(raf);
199 io.disconnect();
200 document.removeEventListener("visibilitychange", start);
201 el.removeEventListener("pointermove", onMove);
202 el.removeEventListener("pointerleave", onLeave);
203 };
204 }, []);
205
206 const typing = typed < prompt.length;
207 const done = lines >= plan.length;
208
209 return (
210 <section ref={root} className="fd-agt" aria-label="Fathom Agent">
211 <style>{css}</style>
212 <div className="fd-agt-bg" aria-hidden="true">
213 <div className="fd-agt-grid" />
214 <div className="fd-agt-beam" />
215 </div>
216 <div ref={copyRef} className="fd-agt-copy">
217 <p className="fd-agt-eyebrow">
218 <span className="fd-agt-dot" aria-hidden="true" />
219 {eyebrow}
220 </p>
221 <h1 className="fd-agt-h1">{title}</h1>
222 <p className="fd-agt-lede">{lede}</p>
223 <div className="fd-agt-ctas">
224 <a className="fd-agt-btn" href={primaryCta.href}>
225 <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
226 <path d="M11.5 1.5 6 6.6 3 4.3 1.5 5v6l1.5.7L6 9.4l5.5 5.1 3-1.4v-10z M11.5 5v6L8 8z M3 6.4 4.7 8 3 9.6z" fill="currentColor" />
227 </svg>
228 {primaryCta.label}
229 </a>
230 <button
231 type="button"
232 className="fd-agt-cli"
233 onClick={() => {
234 navigator.clipboard?.writeText(secondaryCta.label).catch(() => {});
235 setCopied(true);
236 window.setTimeout(() => setCopied(false), 1600);
237 }}
238 aria-label={`Copy command: ${secondaryCta.label}`}
239 >
240 <span className="fd-agt-cli-p">$</span> {secondaryCta.label}
241 <span className="fd-agt-cli-c">{copied ? "Copied" : "Copy"}</span>
242 </button>
243 </div>
244 </div>
245
246 <p className="fd-agt-sr">
247 Example session: the prompt “{prompt}” produces a plan that reads the schema, edits two files, passes 12 tests and renders a billing dashboard preview.
248 </p>
249
250 <div className="fd-agt-stagewrap" aria-hidden="true">
251 <div ref={scene} className="fd-agt-scene">
252 <div ref={prev} className="fd-agt-prev">
253 <div className="fd-agt-chrome">
254 <i /><i /><i />
255 <span className="fd-agt-url">localhost:3000/billing</span>
256 <span className={`fd-agt-hmr ${build > 0 ? "is-on" : ""}`}>HMR</span>
257 </div>
258 <div className="fd-agt-app">
259 <div className="fd-agt-app-h">
260 <b>Billing</b>
261 <span>Oct 2026</span>
262 </div>
263 <div className="fd-agt-kpis">
264 {[
265 ["MRR", "$482.9k", "+8.2%"],
266 ["Net churn", "1.9%", "−0.4 pt"],
267 ["Failed", "37", "$12.4k"],
268 ].map(([k, v, d], i) => (
269 <div key={k} className={`fd-agt-kpi ${build > i ? "is-in" : ""}`} style={{ transitionDelay: `${i * 40}ms` }}>
270 <span>{k}</span>
271 <b>{v}</b>
272 <em>{d}</em>
273 </div>
274 ))}
275 </div>
276 <div className={`fd-agt-chart ${build > 3 ? "is-in" : ""}`}>
277 <div className="fd-agt-chart-h">
278 <span>Monthly recurring revenue</span>
279 <em>12 mo</em>
280 </div>
281 <svg viewBox="0 0 300 90" preserveAspectRatio="none">
282 <defs>
283 <linearGradient id={gid} x1="0" x2="0" y1="0" y2="1">
284 <stop offset="0" stopColor="#7C5CFF" stopOpacity=".35" />
285 <stop offset="1" stopColor="#7C5CFF" stopOpacity="0" />
286 </linearGradient>
287 </defs>
288 <path className="fd-agt-area" d="M0 78 L27 74 L54 70 L81 72 L109 60 L136 56 L163 50 L190 52 L218 38 L245 30 L272 24 L300 12 L300 90 L0 90Z" fill={`url(#${gid})`} />
289 <path className="fd-agt-line" pathLength="1" d="M0 78 L27 74 L54 70 L81 72 L109 60 L136 56 L163 50 L190 52 L218 38 L245 30 L272 24 L300 12" fill="none" stroke="#9D85FF" strokeWidth="2" vectorEffect="non-scaling-stroke" />
290 </svg>
291 </div>
292 <div className={`fd-agt-list ${build > 4 ? "is-in" : ""}`}>
293 {[
294 ["Northwind Bio", "Card declined", "$4,200"],
295 ["Kestrel Air", "Expired card", "$2,950"],
296 ["Oslo Freight", "Insufficient funds", "$1,180"],
297 ].map(([c, r, a], i) => (
298 <div key={c} className="fd-agt-row" style={{ transitionDelay: `${i * 70}ms` }}>
299 <i style={{ background: ["#FF8A65", "#FFC34D", "#64D2FF"][i] }} />
300 <b>{c}</b>
301 <span>{r}</span>
302 <em>{a}</em>
303 </div>
304 ))}
305 </div>
306 <div className={`fd-agt-toast ${build > 5 ? "is-in" : ""}`}>
307 <span className="fd-agt-toast-i">✓</span>
308 Retry scheduled for 37 invoices
309 </div>
310 </div>
311 </div>
312
313 <div ref={term} className="fd-agt-term">
314 <div className="fd-agt-chrome fd-agt-chrome-t">
315 <i /><i /><i />
316 <span className="fd-agt-tab">fathom — ~/acme-billing</span>
317 </div>
318 <div className="fd-agt-body">
319 <div className="fd-agt-prompt">
320 <span className="fd-agt-ps">❯</span>
321 <span>
322 {prompt.slice(0, typed)}
323 {typing && <span className="fd-agt-caret" />}
324 </span>
325 </div>
326 <ol className="fd-agt-plan">
327 {plan.slice(0, lines).map((l, i) => (
328 <li key={i} className={`fd-agt-l fd-agt-l-${l.kind}`}>
329 {l.kind === "think" && (
330 <>
331 <span className="fd-agt-g">✻</span>
332 <span className="fd-agt-think">{l.text}</span>
333 </>
334 )}
335 {l.kind === "tool" && (
336 <>
337 <span className="fd-agt-g">⏺</span>
338 <span>
339 <b>{l.name}</b>(<span className="fd-agt-arg">{l.arg}</span>)
340 </span>
341 {l.meta && <span className="fd-agt-meta">{l.meta}</span>}
342 </>
343 )}
344 {l.kind === "diff" && (
345 <>
346 <span className="fd-agt-g">⏺</span>
347 <span>
348 <b>edit</b> <span className="fd-agt-arg">{l.file}</span>
349 </span>
350 <span className="fd-agt-meta">
351 <span className="fd-agt-add">+{l.add}</span> <span className="fd-agt-del">−{l.del}</span>
352 </span>
353 </>
354 )}
355 {l.kind === "ok" && (
356 <>
357 <span className="fd-agt-g fd-agt-okg">✓</span>
358 <span className="fd-agt-ok">{l.text}</span>
359 </>
360 )}
361 </li>
362 ))}
363 {!typing && !done && (
364 <li className="fd-agt-l fd-agt-working">
365 <span className="fd-agt-spin" /> <span>Working · esc to interrupt</span>
366 </li>
367 )}
368 </ol>
369 {done && (
370 <div className="fd-agt-pr">
371 <span className="fd-agt-pr-b">PR #2481</span>
372 feat(billing): dashboard with MRR, churn &amp; dunning
373 <span className="fd-agt-pr-c">Checks passing</span>
374 </div>
375 )}
376 </div>
377 </div>
378 </div>
379 </div>
380 </section>
381 );
382}
383
384const css = `
385.fd-agt, .fd-agt *, .fd-agt *::before, .fd-agt *::after { box-sizing: border-box; }
386.fd-agt {
387 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
388 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
389 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
390 --fd-dur-1: 120ms; --fd-dur-2: 240ms; --fd-dur-3: 420ms; --fd-dur-4: 700ms;
391 --fd-agt-bg: #07070A;
392 --fd-agt-accent: #9D85FF;
393 --fd-agt-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
394 --fd-agt-sans: "SF Pro Display", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;
395 position: relative; display: block; overflow: hidden; margin: 0; padding: 112px 32px 0; min-height: 880px;
396 background: var(--fd-agt-bg); color: #EDEDF2; font: 400 1rem/1.5 var(--fd-agt-sans);
397 isolation: isolate;
398}
399.fd-agt-bg { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
400.fd-agt-grid {
401 position: absolute; inset: 0;
402 background-image: linear-gradient(rgba(255,255,255,0.045) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.045) 1px, transparent 1px);
403 background-size: 64px 64px; background-position: center top;
404 -webkit-mask-image: radial-gradient(70% 60% at 50% 30%, #000 20%, transparent 75%); mask-image: radial-gradient(70% 60% at 50% 30%, #000 20%, transparent 75%);
405}
406.fd-agt-beam {
407 position: absolute; left: 50%; top: 42%; width: 1100px; height: 640px; transform: translate(-50%, 0);
408 background: radial-gradient(50% 50% at 50% 50%, rgba(124, 92, 255, 0.28), rgba(124,92,255,0.06) 50%, transparent 72%);
409 filter: blur(20px); animation: fd-agt-breathe 7s var(--fd-ease-in-out) infinite alternate;
410}
411.fd-agt-copy { position: relative; z-index: 2; max-width: 860px; margin: 0 auto; text-align: center; will-change: transform, opacity; }
412.fd-agt-eyebrow {
413 display: inline-flex; align-items: center; gap: 10px; margin: 0 0 22px; padding: 7px 14px; border-radius: 9999px;
414 border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.04);
415 font: 500 0.8125rem/1 var(--fd-agt-mono); letter-spacing: -0.01em; color: #B9B9C6;
416 animation: fd-agt-rise var(--fd-dur-4) var(--fd-ease-out) both;
417}
418.fd-agt-dot { display: block; width: 6px; height: 6px; border-radius: 9999px; background: #4ADE80; box-shadow: 0 0 0 3px rgba(74,222,128,0.18); }
419.fd-agt-h1 {
420 margin: 0; padding: 0; color: #F5F5F8;
421 font: 600 clamp(2.75rem, 6vw, 5rem)/0.98 var(--fd-agt-sans); letter-spacing: -0.05em; text-wrap: balance;
422 animation: fd-agt-blurin 900ms var(--fd-ease-out) 80ms both;
423}
424.fd-agt-dim { color: #6E6E7C; }
425.fd-agt-lede {
426 margin: 20px auto 0; padding: 0; max-width: 36rem; color: #9C9CAA;
427 font: 400 clamp(1rem, 1.3vw, 1.1875rem)/1.55 var(--fd-agt-sans); letter-spacing: -0.01em; text-wrap: pretty;
428 animation: fd-agt-rise var(--fd-dur-4) var(--fd-ease-out) 240ms both;
429}
430.fd-agt-ctas { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin: 28px 0 0; animation: fd-agt-rise var(--fd-dur-4) var(--fd-ease-out) 340ms both; }
431.fd-agt-btn {
432 display: inline-flex; align-items: center; gap: 10px; margin: 0; height: 46px; padding: 0 20px; border-radius: 9999px; border: 0;
433 background: #F5F5F8; color: #0B0B0F; text-decoration: none; font: 500 0.9375rem/1 var(--fd-agt-sans); letter-spacing: -0.01em;
434 box-shadow: 0 0 0 1px rgba(255,255,255,0.2), 0 10px 40px -10px rgba(157,133,255,0.6);
435 transition: transform var(--fd-dur-2) var(--fd-spring);
436}
437.fd-agt-btn svg { display: block; }
438.fd-agt-btn:active, .fd-agt-cli:active { transform: scale(0.96); transition-duration: var(--fd-dur-1); }
439.fd-agt-cli {
440 appearance: none; -webkit-appearance: none; cursor: pointer;
441 display: inline-flex; align-items: center; gap: 10px; margin: 0; height: 46px; padding: 0 8px 0 18px; border-radius: 9999px;
442 background: rgba(255,255,255,0.04); color: #E3E3EA; border: 1px solid rgba(255,255,255,0.12);
443 font: 500 0.875rem/1 var(--fd-agt-mono); letter-spacing: -0.01em;
444 transition: transform var(--fd-dur-2) var(--fd-spring), border-color var(--fd-dur-1);
445}
446.fd-agt-cli:hover { border-color: rgba(255,255,255,0.24); }
447.fd-agt-cli-p { color: var(--fd-agt-accent); }
448.fd-agt-cli-c { display: inline-flex; align-items: center; height: 30px; padding: 0 12px; border-radius: 9999px; background: rgba(255,255,255,0.08); color: #B9B9C6; font: 500 0.75rem/1 var(--fd-agt-sans); }
449.fd-agt-btn:focus-visible, .fd-agt-cli:focus-visible { outline: 2px solid rgba(157,133,255,0.7); outline-offset: 2px; }
450.fd-agt-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
451
452.fd-agt-stagewrap { position: relative; max-width: 1180px; margin: 52px auto 0; perspective: 1800px; perspective-origin: 50% 0%; animation: fd-agt-stagein 1200ms var(--fd-ease-out) 300ms both; }
453.fd-agt-scene { position: relative; height: 470px; transform-style: preserve-3d; transform: rotateX(6deg); will-change: transform; }
454.fd-agt-term, .fd-agt-prev {
455 position: absolute; top: 0; border-radius: 16px; overflow: hidden; will-change: transform;
456 border: 1px solid rgba(255,255,255,0.1);
457 box-shadow: 0 1px 0 rgba(255,255,255,0.08) inset, 0 50px 100px -30px rgba(0,0,0,0.9), 0 0 0 1px rgba(0,0,0,0.6);
458}
459.fd-agt-term { left: 0; width: 45%; height: 440px; background: rgba(14, 14, 19, 0.94); -webkit-backdrop-filter: blur(20px); backdrop-filter: blur(20px); transform: translateZ(30px); }
460.fd-agt-prev { right: 0; top: 30px; width: 57%; height: 436px; background: #F6F6F8; transform: translateZ(-20px); color: #0B0B0F; }
461.fd-agt-chrome { display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 14px; border-bottom: 1px solid rgba(11,11,15,0.08); background: #ECECF0; }
462.fd-agt-chrome i { display: block; width: 11px; height: 11px; border-radius: 9999px; background: #D6D6DC; }
463.fd-agt-chrome-t { background: rgba(255,255,255,0.03); border-bottom-color: rgba(255,255,255,0.07); }
464.fd-agt-chrome-t i { background: #2E2E36; }
465.fd-agt-chrome-t i:nth-child(1) { background: #FF5F57; } .fd-agt-chrome-t i:nth-child(2) { background: #FEBC2E; } .fd-agt-chrome-t i:nth-child(3) { background: #28C840; }
466.fd-agt-url { margin: 0 auto; padding: 5px 40px; border-radius: 8px; background: #fff; color: #6B6B76; font: 400 0.75rem/1 var(--fd-agt-mono); }
467.fd-agt-hmr { padding: 4px 7px; border-radius: 6px; font: 600 0.625rem/1 var(--fd-agt-mono); letter-spacing: 0.06em; color: #9A9AA5; background: #E2E2E8; transition: background var(--fd-dur-2), color var(--fd-dur-2); }
468.fd-agt-hmr.is-on { background: #DCFCE7; color: #15803D; }
469.fd-agt-tab { margin: 0 auto; color: #7A7A88; font: 500 0.75rem/1 var(--fd-agt-mono); letter-spacing: -0.01em; transform: translateX(-20px); }
470.fd-agt-body { padding: 20px 20px 18px; font: 400 0.8125rem/1.6 var(--fd-agt-mono); color: #C9C9D4; letter-spacing: -0.01em; }
471.fd-agt-prompt { display: flex; gap: 10px; padding: 12px 14px; border-radius: 10px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); color: #F2F2F6; min-height: 70px; }
472.fd-agt-ps { color: var(--fd-agt-accent); }
473.fd-agt-caret { display: inline-block; width: 8px; height: 1.05em; margin: 0 0 -0.18em 1px; background: #F2F2F6; animation: fd-agt-blink 1s steps(1) infinite; }
474.fd-agt-plan { list-style: none; margin: 14px 0 0; padding: 0; }
475.fd-agt-l { display: flex; gap: 10px; align-items: baseline; margin: 0; padding: 3px 2px; animation: fd-agt-line var(--fd-dur-3) var(--fd-ease-out) both; white-space: nowrap; }
476.fd-agt-l > span:nth-child(2) { overflow: hidden; text-overflow: ellipsis; }
477.fd-agt-g { flex: none; width: 12px; color: #6B6B7A; }
478.fd-agt-l-tool .fd-agt-g, .fd-agt-l-diff .fd-agt-g { color: #4ADE80; }
479.fd-agt-think { color: #8A8A99; font-style: italic; }
480.fd-agt-l b { font-weight: 600; color: #F2F2F6; }
481.fd-agt-arg { color: #C4B5FD; }
482.fd-agt-meta { margin-left: auto; padding-left: 10px; color: #6B6B7A; font-size: 0.75rem; }
483.fd-agt-add { color: #4ADE80; } .fd-agt-del { color: #F87171; }
484.fd-agt-okg { color: #4ADE80; }
485.fd-agt-ok { color: #4ADE80; font-weight: 500; }
486.fd-agt-working { color: #8A8A99; }
487.fd-agt-spin { display: inline-block; flex: none; width: 10px; height: 10px; border-radius: 9999px; border: 1.5px solid #3A3A46; border-top-color: var(--fd-agt-accent); animation: fd-agt-spin 700ms linear infinite; transform: translateY(1px); }
488.fd-agt-pr {
489 display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 14px 0 0; padding: 12px 14px; border-radius: 10px;
490 background: linear-gradient(180deg, rgba(124,92,255,0.14), rgba(124,92,255,0.06)); border: 1px solid rgba(157,133,255,0.3); color: #E6E1FF;
491 font: 500 0.75rem/1.4 var(--fd-agt-mono); animation: fd-agt-pop var(--fd-dur-3) var(--fd-spring) both;
492}
493.fd-agt-pr-b { padding: 3px 7px; border-radius: 6px; background: var(--fd-agt-accent); color: #0B0B0F; font-weight: 700; }
494.fd-agt-pr-c { margin-left: auto; color: #4ADE80; }
495
496.fd-agt-app { position: relative; height: calc(100% - 38px); padding: 18px 20px; font: 400 0.8125rem/1.4 var(--fd-agt-sans); }
497.fd-agt-app-h { display: flex; justify-content: space-between; align-items: baseline; margin: 0 0 14px; }
498.fd-agt-app-h b { font: 600 1.0625rem/1 var(--fd-agt-sans); letter-spacing: -0.02em; }
499.fd-agt-app-h span { color: #8A8A95; font: 500 0.75rem/1 var(--fd-agt-sans); }
500.fd-agt-kpis { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
501.fd-agt-kpi, .fd-agt-chart, .fd-agt-list {
502 border-radius: 12px; background: #fff; box-shadow: 0 0 0 1px rgba(11,11,15,0.06), 0 6px 16px -10px rgba(11,11,15,0.2);
503 opacity: 0; transform: translateY(18px) scale(0.97); transition: opacity var(--fd-dur-3) var(--fd-ease-out), transform var(--fd-dur-4) var(--fd-spring);
504}
505.fd-agt .is-in { opacity: 1; transform: none; }
506.fd-agt-kpi { padding: 12px; }
507.fd-agt-kpi span { display: block; color: #8A8A95; font: 500 0.6875rem/1 var(--fd-agt-sans); }
508.fd-agt-kpi b { display: block; margin: 8px 0 4px; font: 600 1.25rem/1 var(--fd-agt-sans); letter-spacing: -0.03em; color: #0B0B0F; }
509.fd-agt-kpi em { font: 500 0.6875rem/1 var(--fd-agt-sans); font-style: normal; color: #16A34A; }
510.fd-agt-chart { margin: 10px 0 0; padding: 12px 12px 6px; }
511.fd-agt-chart-h { display: flex; justify-content: space-between; color: #6B6B76; font: 500 0.6875rem/1 var(--fd-agt-sans); }
512.fd-agt-chart-h em { font-style: normal; color: #9A9AA5; }
513.fd-agt-chart svg { display: block; width: 100%; height: 92px; margin-top: 6px; }
514.fd-agt-chart .fd-agt-line { stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 1400ms var(--fd-ease-out) 120ms; }
515.fd-agt-chart .fd-agt-area { opacity: 0; transition: opacity 900ms var(--fd-ease-out) 500ms; }
516.fd-agt-chart.is-in .fd-agt-line { stroke-dashoffset: 0; }
517.fd-agt-chart.is-in .fd-agt-area { opacity: 1; }
518.fd-agt-list { margin: 10px 0 0; padding: 4px 12px; }
519.fd-agt-row { display: grid; grid-template-columns: 8px 1.1fr 1.2fr auto; align-items: center; gap: 10px; padding: 8px 0; border-top: 1px solid rgba(11,11,15,0.06); opacity: 0; transform: translateX(-10px); transition: opacity var(--fd-dur-3) var(--fd-ease-out), transform var(--fd-dur-3) var(--fd-ease-out); }
520.fd-agt-row:first-child { border-top: 0; }
521.fd-agt-list.is-in .fd-agt-row { opacity: 1; transform: none; }
522.fd-agt-row i { display: block; width: 8px; height: 8px; border-radius: 9999px; }
523.fd-agt-row b { font: 600 0.75rem/1 var(--fd-agt-sans); color: #1C1C22; }
524.fd-agt-row span { font: 400 0.75rem/1 var(--fd-agt-sans); color: #8A8A95; }
525.fd-agt-row em { font: 500 0.75rem/1 var(--fd-agt-mono); font-style: normal; color: #1C1C22; }
526.fd-agt-toast {
527 position: absolute; right: 18px; bottom: 18px; display: flex; align-items: center; gap: 10px; padding: 10px 14px 10px 10px; border-radius: 12px;
528 background: #0B0B0F; color: #F2F2F6; font: 500 0.75rem/1 var(--fd-agt-sans); box-shadow: 0 16px 40px -12px rgba(11,11,15,0.6);
529 opacity: 0; transform: translateY(16px) scale(0.94); transition: opacity var(--fd-dur-3) var(--fd-ease-out), transform var(--fd-dur-3) var(--fd-spring);
530}
531.fd-agt-toast.is-in { opacity: 1; transform: none; }
532.fd-agt-toast-i { display: grid; place-items: center; width: 20px; height: 20px; border-radius: 9999px; background: #22C55E; color: #0B0B0F; font: 700 0.6875rem/1 var(--fd-agt-sans); }
533
534@keyframes fd-agt-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
535@keyframes fd-agt-blurin { from { opacity: 0; filter: blur(14px); transform: translateY(10px) scale(0.98); } to { opacity: 1; filter: none; transform: none; } }
536@keyframes fd-agt-stagein { from { opacity: 0; transform: translateY(80px) scale(0.94); filter: blur(8px); } to { opacity: 1; transform: none; filter: none; } }
537@keyframes fd-agt-line { from { opacity: 0; transform: translateY(6px); filter: blur(3px); } to { opacity: 1; transform: none; filter: none; } }
538@keyframes fd-agt-pop { from { opacity: 0; transform: scale(0.94); } to { opacity: 1; transform: none; } }
539@keyframes fd-agt-blink { 50% { opacity: 0; } }
540@keyframes fd-agt-spin { to { transform: translateY(1px) rotate(360deg); } }
541@keyframes fd-agt-breathe { from { opacity: 0.75; transform: translate(-50%, 0) scale(0.96); } to { opacity: 1; transform: translate(-50%, -2%) scale(1.04); } }
542
543@media (max-width: 900px) {
544 .fd-agt { padding: 104px 16px 0; min-height: 844px; }
545 .fd-agt-stagewrap { margin-top: 44px; }
546 .fd-agt-scene { height: 540px; }
547 .fd-agt-term { width: 100%; height: 330px; }
548 .fd-agt-prev { left: 8%; right: auto; width: 92%; top: 220px; height: 400px; }
549 .fd-agt-body { padding: 14px; font-size: 0.75rem; }
550 .fd-agt-meta { display: none; }
551 .fd-agt-url { padding: 5px 14px; }
552 .fd-agt-cli { display: none; }
553 .fd-agt-eyebrow { font-size: 0.6875rem; letter-spacing: -0.02em; white-space: nowrap; padding: 7px 12px; }
554}
555@media (prefers-reduced-motion: reduce) {
556 .fd-agt *, .fd-agt *::before { animation: none !important; transition-duration: 1ms !important; transition-delay: 0ms !important; }
557}
558@media (prefers-reduced-transparency: reduce) { .fd-agt-term { background: #0E0E13; -webkit-backdrop-filter: none; backdrop-filter: none; } }
559`;
560
561/* ───────────────────────── Demo ───────────────────────── */
562
563export function Demo() {
564 return (
565 <div className="fd-agt-demo">
566 <style>{demoCss}</style>
567 <header className="fd-agt-demo-nav">
568 <a className="fd-agt-demo-logo" href="#top">
569 <svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true">
570 <path d="M3 3h14v4H7v3h8v4H7v3H3z" fill="#F5F5F8" />
571 </svg>
572 fathom
573 </a>
574 <nav aria-label="Primary">
575 <a href="#product">Product</a>
576 <a href="#docs">Docs</a>
577 <a href="#changelog">Changelog</a>
578 <a href="#enterprise">Enterprise</a>
579 <a href="#pricing">Pricing</a>
580 </nav>
581 <div className="fd-agt-demo-r">
582 <a href="#gh" className="fd-agt-demo-gh">
583 <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M8 .2l2.4 4.9 5.4.8-3.9 3.8.9 5.4L8 12.6l-4.8 2.5.9-5.4L.2 5.9l5.4-.8z" /></svg>
584 41.2k
585 </a>
586 <a href="#login" className="fd-agt-demo-in">Log in</a>
587 </div>
588 </header>
589 <HeroAiTerminal />
590 </div>
591 );
592}
593
594const demoCss = `
595.fd-agt-demo { position: relative; margin: 0; padding: 0; min-height: 100vh; background: #07070A; }
596.fd-agt-demo *, .fd-agt-demo *::before, .fd-agt-demo *::after { box-sizing: border-box; }
597.fd-agt-demo-nav {
598 position: absolute; z-index: 10; top: 0; left: 0; right: 0; max-width: 1180px; margin: 0 auto; height: 68px; padding: 0 32px;
599 display: flex; align-items: center; justify-content: space-between; gap: 20px;
600 border-bottom: 1px solid rgba(255,255,255,0.06);
601}
602.fd-agt-demo-nav .fd-agt-demo-logo { display: inline-flex; align-items: center; gap: 9px; margin: 0; color: #F5F5F8; text-decoration: none; font: 600 1.0625rem/1 ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: -0.04em; }
603.fd-agt-demo-logo svg { display: block; }
604.fd-agt-demo-nav nav { display: flex; gap: 28px; }
605.fd-agt-demo-nav a { color: #9C9CAA; text-decoration: none; font: 500 0.875rem/1 ui-sans-serif, -apple-system, "Inter", sans-serif; letter-spacing: -0.005em; transition: color 120ms; }
606.fd-agt-demo-nav a:hover { color: #F5F5F8; }
607.fd-agt-demo-nav a:focus-visible { outline: 2px solid rgba(157,133,255,.7); outline-offset: 2px; border-radius: 6px; }
608.fd-agt-demo-r { display: flex; align-items: center; gap: 18px; }
609.fd-agt-demo-gh { display: inline-flex; align-items: center; gap: 6px; font-family: ui-monospace, "SF Mono", Menlo, monospace !important; }
610.fd-agt-demo-gh svg { display: block; color: #FEBC2E; }
611.fd-agt-demo-in { display: inline-flex; align-items: center; height: 32px; padding: 0 14px; border-radius: 9999px; border: 1px solid rgba(255,255,255,0.14); color: #F5F5F8 !important; }
612@media (max-width: 900px) { .fd-agt-demo-nav { padding: 0 16px; } .fd-agt-demo-nav nav, .fd-agt-demo-gh { display: none !important; } }
613`;

More Heroes. Same file contract.

Ledger Card Hero

A photoreal CSS-3D metal card that tilts to the cursor, then flips and docks on scroll while spend stats count up.

Maison Editorial Hero

A giant serif masthead and procedural duotone fabric frames revealed by clip-path wipes, drifting apart on scroll.

Fjord Parallax Hero

A layered SVG landscape with deep pointer and scroll parallax, a mirrored lake and golden hour fading to dusk.