"use client"; import { useEffect, useId, useState, type ReactNode } from "react"; /* * Agent Team – an animated card illustration, in a single self-contained file. * A prompt types itself into a pill and is sent; dashed lines branch out from it to three agents, * who pop in one after another with their role and the tools they use – then the scene starts over. * The artwork is laid out on a 600 × 400 grid inside a 3 : 2 frame and scales with its container. */ export type AgentTool = "linkedin" | "hubspot" | "slack" | "gmail" | "salesforce"; export type AgentTeamMember = { /** Role shown in the pill */ role: string; /** Square portrait, shown in a circle */ avatar: string; /** One or two tools, drawn as overlapping logo circles */ tools?: AgentTool[]; }; export type AgentTeamProps = { /** Text that types itself into the prompt */ prompt?: string; /** Exactly three agents, left to right */ members: [AgentTeamMember, AgentTeamMember, AgentTeamMember]; className?: string; /** Accessible description of the scene */ label?: string; }; /* Choreography, in ms */ const TYPE = 22; // per character const SEND = 420; // button press after typing const SPREAD = 700; // lines draw down const STAGGER = 160; // between agents const HOLD = 2600; // finished team stays const RESET = 600; // fade back type Phase = "typing" | "sent" | "team" | "reset"; export function AgentTeam({ prompt = "Build a team to work my inbound pipeline", members, className, label }: AgentTeamProps) { const id = "ui-at" + useId().replace(/[^a-zA-Z0-9_-]/g, ""); const [typed, setTyped] = useState(0); const [phase, setPhase] = useState("typing"); useEffect(() => { // Reduced motion: show the finished scene and stay there if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setTyped(prompt.length); setPhase("team"); return; } const timers: number[] = []; const later = (fn: () => void, ms: number) => timers.push(window.setTimeout(fn, ms)); const run = () => { setPhase("typing"); setTyped(0); for (let i = 1; i <= prompt.length; i++) later(() => setTyped(i), 500 + i * TYPE); const typedAt = 500 + prompt.length * TYPE; later(() => setPhase("sent"), typedAt + SEND); later(() => setPhase("team"), typedAt + SEND + SPREAD * 0.55); const doneAt = typedAt + SEND + SPREAD + STAGGER * 3 + 500; later(() => setPhase("reset"), doneAt + HOLD); later(run, doneAt + HOLD + RESET); }; run(); return () => timers.forEach((t) => window.clearTimeout(t)); }, [prompt]); const lines = ["M300 110C300 162 102 152 102 196", "M300 110V196", "M300 110C300 162 498 152 498 196"]; return (
m.role).join(", ")}`} > {/* React 19 hoists this into once, no matter how many instances render */} {/* Soft Pro-colored glow that blooms behind the prompt when it's sent */} ); } /* ---------- Tool logos: the brands' own marks (svgl.app, Simple Icons) on a 24 grid ---------- */ const Svg = ({ children }: { children: ReactNode }) => ( ); const logos: Record = { linkedin: ( ), hubspot: ( ), slack: ( ), gmail: ( ), salesforce: ( ), }; /* ---------- Styles ---------- */ const css = /* css */ ` .ui-at { /* 1 unit of the 600 × 400 layout grid */ --u: calc(100cqw / 600); --ui-at-surface: #ffffff; --ui-at-ring: rgba(10, 10, 10, 0.06); --ui-at-text: #171717; --ui-at-accent: #2f6bff; --ui-at-caret: #171717; /* Same pink → violet as the Get Pro button */ --ui-at-halo-from: rgba(221, 6, 116, 0.18); --ui-at-halo-to: rgba(94, 62, 224, 0.18); --ui-at-dash: #e4e7ec; /* same gray as the lines in Outreach Branches */ --ui-at-face: #eef0f4; --ui-at-online: #34c46a; --ui-at-dot-ring: #e2f6e9; --ui-at-dot-edge: rgba(52, 196, 106, 0.1); --ui-at-tool-ring: #e5e5e5; --ui-at-shadow: 0 calc(10 * var(--u)) calc(24 * var(--u)) rgba(0, 0, 0, 0.07), 0 calc(2 * var(--u)) calc(5 * var(--u)) rgba(0, 0, 0, 0.05); --ui-at-ease: cubic-bezier(0.7, 0, 0.25, 1); --ui-at-pop: cubic-bezier(0.34, 1.4, 0.64, 1); position: relative; width: 100%; aspect-ratio: 3 / 2; overflow: hidden; container-type: inline-size; font-family: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif; -webkit-font-smoothing: antialiased; } .ui-at *, .ui-at *::before, .ui-at *::after { box-sizing: border-box; } /* ---------- Dashed lines, revealed top to bottom ---------- */ .ui-at-lines { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; } .ui-at-line { fill: none; stroke: var(--ui-at-dash); stroke-width: 2.4; stroke-linecap: round; stroke-dasharray: 6 7.23; } .ui-at-reveal { fill: none; stroke: #fff; stroke-width: 8; stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 700ms var(--ui-at-ease); } .ui-at:is([data-phase="sent"], [data-phase="team"]) .ui-at-reveal { stroke-dashoffset: 0; } .ui-at[data-phase="reset"] .ui-at-lines { opacity: 0; transition: opacity 500ms ease; } /* ---------- Prompt ---------- */ .ui-at-halo { position: absolute; left: 50%; top: calc(52 * var(--u)); width: calc(400 * var(--u)); height: calc(68 * var(--u)); transform: translateX(-50%) scale(0.9); border-radius: 999px; background: linear-gradient(90deg, var(--ui-at-halo-from), var(--ui-at-halo-to)); filter: blur(calc(22 * var(--u))); opacity: 0; transition: opacity 700ms ease, transform 900ms var(--ui-at-ease); } .ui-at:is([data-phase="sent"], [data-phase="team"]) .ui-at-halo { opacity: 1; transform: translateX(-50%) scale(1); } .ui-at-prompt { position: absolute; left: 50%; top: calc(60 * var(--u)); display: flex; align-items: center; gap: calc(12 * var(--u)); width: calc(380 * var(--u)); height: calc(52 * var(--u)); padding: 0 calc(8 * var(--u)) 0 calc(20 * var(--u)); transform: translateX(-50%); border-radius: 999px; background: var(--ui-at-surface); box-shadow: 0 0 0 calc(1 * var(--u)) var(--ui-at-ring), var(--ui-at-shadow); } .ui-at-text { flex: 1; min-width: 0; overflow: hidden; font-size: calc(15 * var(--u)); line-height: 1.3; letter-spacing: -0.01em; white-space: nowrap; color: var(--ui-at-text); } .ui-at-caret { display: inline-block; width: calc(1.6 * var(--u)); height: calc(17 * var(--u)); margin-left: calc(2 * var(--u)); vertical-align: calc(-3 * var(--u)); border-radius: 1px; background: var(--ui-at-caret); animation: ui-at-blink 1s steps(1) infinite; } .ui-at-send { flex: none; display: grid; place-items: center; width: calc(36 * var(--u)); height: calc(36 * var(--u)); border-radius: 50%; background: var(--ui-at-accent); transition: transform 200ms var(--ui-at-pop), filter 200ms ease; } .ui-at-send svg { width: calc(18 * var(--u)); height: calc(18 * var(--u)); fill: none; stroke: #fff; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; } .ui-at[data-phase="sent"] .ui-at-send { animation: ui-at-press 380ms var(--ui-at-ease); } .ui-at[data-phase="reset"] .ui-at-text { opacity: 0; transition: opacity 400ms ease; } /* ---------- Agents ---------- */ .ui-at-agent { position: absolute; top: calc(196 * var(--u)); display: flex; flex-direction: column; align-items: center; gap: calc(14 * var(--u)); transform: translateX(-50%); } .ui-at-node, .ui-at-role { opacity: 0; transform: translateY(calc(10 * var(--u))) scale(0.92); transition: opacity 360ms ease, transform 520ms var(--ui-at-pop); } .ui-at[data-phase="team"] .ui-at-node { opacity: 1; transform: none; transition-delay: calc(var(--i) * 160ms); } .ui-at[data-phase="team"] .ui-at-role { opacity: 1; transform: none; transition-delay: calc(var(--i) * 160ms + 120ms); } .ui-at[data-phase="reset"] .ui-at-node, .ui-at[data-phase="reset"] .ui-at-role { transform: none; transition: opacity 500ms ease; } .ui-at-node { position: relative; width: calc(84 * var(--u)); height: calc(84 * var(--u)); padding: calc(5 * var(--u)); border-radius: 50%; background: var(--ui-at-surface); box-shadow: inset 0 0 0 calc(1 * var(--u)) var(--ui-at-ring), var(--ui-at-shadow); } .ui-at-face { display: block; width: 100%; height: 100%; overflow: hidden; border-radius: 50%; background: var(--ui-at-face); } .ui-at-face img { display: block; width: 100%; height: 100%; object-fit: cover; user-select: none; } .ui-at-dot { position: absolute; right: calc(1 * var(--u)); bottom: calc(3 * var(--u)); width: calc(24 * var(--u)); height: calc(24 * var(--u)); border: calc(7 * var(--u)) solid var(--ui-at-dot-ring); border-radius: 50%; background: var(--ui-at-online); box-shadow: 0 0 0 calc(2 * var(--u)) var(--ui-at-dot-edge); transform: scale(0); transition: transform 420ms var(--ui-at-pop); } .ui-at[data-phase="team"] .ui-at-dot { transform: scale(1); transition-delay: calc(var(--i) * 160ms + 360ms); } /* Role pill: overlapping tool circles first, then the role */ .ui-at-role { display: flex; align-items: center; gap: calc(10 * var(--u)); height: calc(42 * var(--u)); padding: 0 calc(15 * var(--u)) 0 calc(7 * var(--u)); border-radius: 999px; background: var(--ui-at-surface); box-shadow: 0 0 0 calc(1 * var(--u)) var(--ui-at-ring), var(--ui-at-shadow); font-size: calc(12.5 * var(--u)); font-weight: 500; letter-spacing: -0.01em; white-space: nowrap; color: var(--ui-at-text); } .ui-at-role:not(:has(.ui-at-tools)) { padding-left: calc(15 * var(--u)); } .ui-at-tools { display: flex; } .ui-at-tool { display: grid; place-items: center; width: calc(26 * var(--u)); height: calc(26 * var(--u)); border-radius: 50%; background: var(--ui-at-surface); box-shadow: 0 0 0 calc(1 * var(--u)) var(--ui-at-tool-ring), 0 calc(1.5 * var(--u)) calc(3 * var(--u)) rgba(0, 0, 0, 0.08); } /* The second circle sits slightly over the first */ .ui-at-tool + .ui-at-tool { margin-left: calc(-8 * var(--u)); } .ui-at-tool svg { width: calc(14 * var(--u)); height: calc(14 * var(--u)); } @keyframes ui-at-blink { 50% { opacity: 0; } } @keyframes ui-at-press { 0% { transform: scale(1); } 40% { transform: scale(0.86); } 100% { transform: scale(1); } } @media (prefers-reduced-motion: reduce) { .ui-at *, .ui-at *::before { transition: none !important; animation: none !important; } } /* Dark mode: follows a .dark class or data-theme="dark" on any ancestor, e.g. */ :where(.dark, [data-theme="dark"]) .ui-at { --ui-at-surface: #232326; --ui-at-ring: rgba(255, 255, 255, 0.08); --ui-at-text: #ededed; --ui-at-accent: #2f6bff; --ui-at-caret: #ededed; --ui-at-halo-from: rgba(244, 114, 182, 0.16); --ui-at-halo-to: rgba(167, 139, 250, 0.16); --ui-at-dash: rgba(255, 255, 255, 0.14); --ui-at-face: #2e2e33; --ui-at-online: #3fd17a; --ui-at-dot-ring: #24392c; --ui-at-dot-edge: rgba(63, 209, 122, 0.12); --ui-at-tool-ring: rgba(255, 255, 255, 0.12); --ui-at-shadow: 0 calc(10 * var(--u)) calc(24 * var(--u)) rgba(0, 0, 0, 0.4), 0 calc(2 * var(--u)) calc(5 * var(--u)) rgba(0, 0, 0, 0.3); } :where(.dark, [data-theme="dark"]) .ui-at-tool { background: #f4f4f5; } `;