nextui.dev
Home
Docs
Saved
Pricing
Components37
  • Forms & Inputs7
  • Cards13
  • Buttons, Badges & Avatars8
  • Feedback & Display6
  • Effects3
Agents46
  • Input & Context9
  • Thinking & Status6
  • Tools & Execution7
  • Human-in-the-Loop & Safety7
  • Multi-Agent & Workflows4
  • Output & Artifacts8
  • Voice & Memory5
Blocks20
  • Agent chatNew
  • Pill heroNew
  • Grid hero
  • Moodboard generatorNew
  • Waitlist heroNew
  • Intro bannerNew
  • Creative home heroNew
  • Chat heroNew
  • App shelfNew
  • Usage card
  • Design resultsNew
  • Pricing tiers
  • Dashboard home
  • The shiftNew
  • Launch perksNew
  • Testimonials
  • Quote
  • Team
  • Leads tableNew
  • Sign in
Pages16
  • ASCII workspace$49
  • Research agent$49
  • Support agent$49
  • Coding agent$49
  • CRM overviewNew
  • Marketing dashboardNew
  • Developer portfolio$49
  • Particle landing$49
  • Particle story$49
  • Statement converter$49
  • AI photo studio$49
  • Group card launch$49
  • Month-end storyNew
  • Task HubNew
  • Global MediaNew
  • Hexora ecosystem$49
Themes9
  • Default
  • Ember
  • Ocean
  • Forest
  • Grape
  • Rose
  • Amber
  • Clay
  • Brutal
UpgradeDark modeLight modeFeedbackGitHub
nextui.dev

nextui components, blocks and themes you install with one command.

Product

  • Components
  • Blocks
  • Themes
  • Pricing

Resources

  • Docs
  • Roadmap
  • GitHub

Legal

  • License
  • Privacy
  • Terms
  • Refunds

nextUI

[ 200 OK ] · nextui.dev

© 2026 nextui.dev • All rights reserved.

UpgradeDark modeLight modeFeedbackGitHub
ComponentsAI & AgentsBrowser agent
Copy promptCopy prompt

Customize

Current URL shown in the address bar

Execution state of the browser session

Loop the scripted browser session

JSX · copied with Copy prompt

<BrowserAgent autoPlay />

Browser agentNew

Headless browser viewport for a computer-use AI agent: live address bar, animated agent cursor with click ripples, step progress dots, and a Take over button for manual human input.

Installation

Set your access key from API keys as NEXTUI_TOKEN in .env.local and add the @nextui namespace below to components.json once. Then every item installs by name. Each install counts as a copy on a free account.

$pnpm dlx shadcn@latest add @nextui/browser-agent
1{
2 "registries": {
3 "@nextui": {
4 "url": "https://nextui.dev/r/{name}.json",
5 "headers": { "Authorization": "Bearer ${NEXTUI_TOKEN}" }
6 }
7 }
8}

Usage

1"use client";
2
3import { BrowserAgent, type BrowserAgentStep } from "@/components/nextui/browser-agent";
4
5export function WebOperator({ steps }: { steps: BrowserAgentStep[] }) {
6 return (
7 <BrowserAgent
8 url="https://portal.northwind.test/orders"
9 steps={steps}
10 onToggleControl={(manual) => console.log("Manual takeover:", manual)}
11 />
12 );
13}

API reference

Edit props live in the preview above, or see all 7 props
PropTypeDefaultDescription
urlstring"https://portal.northwind.test/orders"Current URL shown in the address bar
status"running" | "paused" | "done"–Execution state of the browser session
stepsBrowserAgentStep[]–Sequence of navigation, click, type and extract steps with cursor coordinates
onToggleControl(userControlling: boolean) => void–Called when the user clicks Take over or Resume
screenshotReactNode–Custom page content rendered inside the viewport instead of the scripted scene
autoPlaybooleanfalseLoop the scripted browser session
classNamestring–Extra classes on the wrapper

Related

All components

Chat inputNew

Task checklistNew

Free

ReasoningNew

components/nextui/browser-agent.tsx
"use client";

import { useEffect, useId, useRef, useState } from "react";
import { Camera, Check, Hand, Lock, MousePointer2, Play, RotateCw, ShieldAlert } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type BrowserAgentStep = {
  id: string;
  action: "navigate" | "click" | "type" | "extract" | "wait" | "done";
  label: string;
  url?: string;
  /** Screenshot of the page after this step, shown in the Steps view. */
  screenshot?: string;
};

export type BrowserAgentProps = {
  url?: string;
  status?: "running" | "paused" | "done";
  /** Action log. Leave unset for the built-in scripted demo. */
  steps?: BrowserAgentStep[];
  /** Live frame of the remote browser (image URL). Leave unset to show the demo page. */
  screenshot?: string;
  /** Called when the user takes over (true) or hands control back (false). */
  onToggleControl?: (userControlling: boolean) => void;
  /** Loop a scripted sign-in → 2FA → extract demo. */
  autoPlay?: boolean;
  className?: string;
};

const EASE = [0.22, 1, 0.36, 1] as const;
const TICK = 100;
const LOOP = 12400;
const HOST = "portal.northwind.test";
const EMAIL = "ops@studio.test";

const SCRIPT: (BrowserAgentStep & { at: number })[] = [
  { id: "1", at: 300, action: "navigate", label: `Opened ${HOST}/login`, url: `https://${HOST}/login` },
  { id: "2", at: 2700, action: "type", label: `Typed “${EMAIL}” into Email` },
  { id: "3", at: 3500, action: "click", label: "Clicked “Sign in”" },
  { id: "4", at: 3900, action: "wait", label: "2FA required · handed to you" },
  { id: "5", at: 6600, action: "navigate", label: `Opened ${HOST}/orders`, url: `https://${HOST}/orders` },
  { id: "6", at: 9300, action: "extract", label: "Extracted 12 rows" },
];

function useDemoClock(enabled: boolean, loop: number) {
  const ref = useRef<HTMLDivElement>(null);
  const [visible, setVisible] = useState(false);
  const [elapsed, setElapsed] = useState(0);
  useEffect(() => {
    const el = ref.current;
    if (!el || !enabled) return;
    const io = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting));
    io.observe(el);
    return () => io.disconnect();
  }, [enabled]);
  useEffect(() => {
    if (!enabled || !visible) return;
    const id = setInterval(() => setElapsed((e) => e + TICK), TICK);
    return () => clearInterval(id);
  }, [enabled, visible]);
  return { ref, t: elapsed % loop };
}

type Box = { x: number; y: number; w: number; h: number; label: string };
type Scene = {
  url: string;
  loading: number | null;
  page: "login" | "2fa" | "orders";
  email: string;
  code: number;
  cursor: [number, number];
  click: number | null;
  box: Box | null;
  human: boolean;
  scan: number;
  extracted: boolean;
};

const EMAIL_BOX: Box = { x: 30, y: 37, w: 40, h: 13, label: "input#email" };
const BUTTON_BOX: Box = { x: 30, y: 70, w: 40, h: 13, label: "button “Sign in”" };
const TABLE_BOX: Box = { x: 3, y: 31, w: 94, h: 56, label: "table.orders · 12 rows" };

function sceneAt(t: number): Scene {
  const s: Scene = {
    url: `https://${HOST}/login`,
    loading: t < 1000 ? Math.min(1, t / 900) : null,
    page: "login",
    email: "",
    code: 0,
    cursor: [82, 14],
    click: null,
    box: null,
    human: false,
    scan: -1,
    extracted: false,
  };
  if (t >= 1300) s.cursor = [44, 44];
  if (t >= 1300 && t < 2800) s.box = EMAIL_BOX;
  if (t >= 1800) s.click = 1800;
  if (t >= 1900) s.email = EMAIL.slice(0, Math.min(EMAIL.length, Math.floor((t - 1900) / 50) + 1));
  if (t >= 2900) {
    s.cursor = [52, 77];
    s.box = BUTTON_BOX;
  }
  if (t >= 3500) s.click = 3500;
  if (t >= 3900) {
    s.page = "2fa";
    s.box = null;
    s.cursor = [74, 30];
  }
  if (t >= 4900 && t < 6200) s.human = true;
  if (t >= 5100) s.code = Math.min(6, Math.floor((t - 5100) / 150) + 1);
  if (t >= 6600) {
    s.page = "orders";
    s.url = `https://${HOST}/orders`;
    s.loading = t < 7300 ? Math.min(1, (t - 6600) / 600) : null;
    s.cursor = [70, 16];
  }
  if (t >= 7500) {
    s.cursor = [22, 37];
    s.box = TABLE_BOX;
  }
  if (t >= 7700) {
    s.scan = Math.min(4, Math.floor((t - 7700) / 320));
    s.cursor = [22, 37 + s.scan * 10];
  }
  if (t >= 9300) {
    s.extracted = true;
    s.box = null;
    s.scan = -1;
    s.cursor = [62, 8];
  }
  return s;
}

const FINAL = sceneAt(LOOP - 100);
const SNAP_AT: Record<string, number> = { "1": 1200, "2": 2800, "3": 3600, "4": 4500, "5": 7400, "6": 9600 };

/** A browser viewport for a computer-use agent: URL bar, a moving agent cursor with click ripples, a live action log, Take over for CAPTCHA / 2FA, and a step history. */
export function BrowserAgent({
  url,
  status: controlledStatus,
  steps: stepsProp,
  screenshot,
  onToggleControl,
  autoPlay = false,
  className,
}: BrowserAgentProps) {
  const reduce = useReducedMotion() ?? false;
  const uid = useId();
  const [touched, setTouched] = useState(false);
  const [userControl, setUserControl] = useState(false);
  const [tab, setTab] = useState<"live" | "steps">("live");
  const [picked, setPicked] = useState<string | null>(null);
  const demo = autoPlay && !reduce && !touched;
  const { ref, t } = useDemoClock(demo, LOOP);

  const liveScene = demo ? sceneAt(t) : FINAL;
  const steps: BrowserAgentStep[] = stepsProp ?? (demo ? SCRIPT.filter((s) => t >= s.at) : SCRIPT);
  const pickedStep = steps.find((s) => s.id === picked) ?? steps[steps.length - 1];
  const viewing = tab === "steps" && pickedStep;
  const scene = viewing && SNAP_AT[pickedStep.id] !== undefined && !stepsProp ? { ...sceneAt(SNAP_AT[pickedStep.id]), click: null, loading: null } : liveScene;
  const human = userControl || (!viewing && scene.human);
  const needsHuman = !viewing && scene.page === "2fa" && !human;
  const shownUrl = viewing ? (pickedStep.url ?? scene.url) : (url ?? scene.url);
  const status =
    controlledStatus ?? (human || needsHuman ? "paused" : demo ? (scene.extracted ? "done" : "running") : "done");

  const toggleControl = () => {
    setTouched(true);
    const next = !userControl;
    setUserControl(next);
    onToggleControl?.(next);
  };

  const lastLog = steps.slice(-3);

  return (
    <div ref={ref} className={cn("w-full max-w-md text-sm", className)}>
      <div className="overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-[0_1px_2px_rgb(0_0_0/0.04),0_12px_32px_-18px_rgb(0_0_0/0.25)]">
        {/* Chrome */}
        <div className="relative flex items-center gap-2 border-b bg-gradient-to-b from-muted/60 to-muted/20 px-3 py-2">
          <div className="flex gap-1.5" aria-hidden>
            <span className="size-2.5 rounded-full bg-foreground/15" />
            <span className="size-2.5 rounded-full bg-foreground/15" />
            <span className="size-2.5 rounded-full bg-foreground/15" />
          </div>
          <div className="flex h-7 min-w-0 flex-1 items-center gap-1.5 rounded-lg border bg-background px-2 shadow-xs">
            <Lock className="size-3 shrink-0 text-emerald-600 dark:text-emerald-400" aria-hidden />
            <AnimatePresence mode="popLayout" initial={false}>
              <motion.span
                key={shownUrl}
                initial={reduce ? false : { opacity: 0, y: 6 }}
                animate={{ opacity: 1, y: 0 }}
                exit={reduce ? undefined : { opacity: 0, y: -6 }}
                transition={{ duration: 0.25, ease: EASE }}
                className="truncate font-mono text-[11px]"
              >
                <span className="text-foreground">{shownUrl.replace(/^https?:\/\//, "").split("/")[0]}</span>
                <span className="text-muted-foreground">/{shownUrl.replace(/^https?:\/\//, "").split("/").slice(1).join("/")}</span>
              </motion.span>
            </AnimatePresence>
            <RotateCw className={cn("ml-auto size-3 shrink-0 text-muted-foreground", scene.loading !== null && !reduce && "animate-spin")} aria-hidden />
          </div>
          <div className="relative shrink-0">
            {needsHuman && !reduce && <span className="absolute -inset-1 animate-ping rounded-lg bg-amber-500/30" aria-hidden />}
            <button
              type="button"
              onClick={toggleControl}
              className={cn(
                "relative inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-lg border px-2.5 text-xs font-medium shadow-xs outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring",
                human ? "border-foreground bg-foreground text-background" : needsHuman ? "border-amber-500/60 bg-amber-500 text-white" : "bg-background hover:bg-muted",
              )}
            >
              {human ? <Play className="size-3" aria-hidden /> : <Hand className="size-3" aria-hidden />}
              {human ? "Resume agent" : "Take over"}
            </button>
          </div>
          {scene.loading !== null && (
            <span className="absolute inset-x-0 bottom-0 h-0.5 overflow-hidden" aria-hidden>
              <span className="block h-full bg-foreground transition-[width] duration-100" style={{ width: `${scene.loading * 100}%` }} />
            </span>
          )}
        </div>

        {/* Viewport */}
        <div className="relative h-52 overflow-hidden bg-background select-none">
          {screenshot ? (
            // eslint-disable-next-line @next/next/no-img-element
            <img src={screenshot} alt="Live view of the agent's browser" className="size-full object-cover object-top" />
          ) : (
            <DemoPage scene={scene} reduce={reduce} />
          )}

          {/* Target box */}
          <AnimatePresence>
            {scene.box && !human && (
              <motion.div
                key={scene.box.label}
                initial={reduce ? false : { opacity: 0, scale: 1.08 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={{ opacity: 0 }}
                transition={{ duration: 0.3, ease: EASE }}
                className="pointer-events-none absolute rounded-md border-[1.5px] border-dashed border-sky-500 bg-sky-500/[0.06]"
                style={{ left: `${scene.box.x}%`, top: `${scene.box.y}%`, width: `${scene.box.w}%`, height: `${scene.box.h}%` }}
              >
                <span className="absolute -top-[18px] left-0 rounded bg-sky-500 px-1 font-mono text-[9px] leading-4 whitespace-nowrap text-white">
                  {scene.box.label}
                </span>
              </motion.div>
            )}
          </AnimatePresence>

          {/* Agent cursor */}
          {!human && !viewing && (
            <motion.div
              className="pointer-events-none absolute z-10"
              initial={false}
              animate={{ left: `${scene.cursor[0]}%`, top: `${scene.cursor[1]}%` }}
              transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 140, damping: 20, mass: 0.8 }}
            >
              {scene.click !== null && !reduce && (
                <span key={scene.click} className="absolute -top-3 -left-3 size-6" aria-hidden>
                  <span className="ba-ripple absolute inset-0 rounded-full border-2 border-sky-500" />
                  <span className="ba-ripple absolute inset-0 rounded-full bg-sky-500/30 [animation-delay:90ms]" />
                </span>
              )}
              <MousePointer2 className="size-[18px] -translate-x-[3px] -translate-y-[2px] fill-foreground text-background drop-shadow-[0_2px_3px_rgb(0_0_0/0.3)]" aria-hidden />
              <span className="absolute top-3.5 left-3 rounded-full bg-sky-500 px-1.5 py-px text-[9px] font-semibold whitespace-nowrap text-white shadow-sm">Agent</span>
            </motion.div>
          )}

          {/* Human control */}
          <AnimatePresence>
            {human && (
              <motion.div
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                className="pointer-events-none absolute inset-0 ring-[3px] ring-amber-500 ring-inset"
              >
                <span className="absolute top-2 left-1/2 -translate-x-1/2 rounded-full bg-amber-500 px-2.5 py-0.5 text-[10px] font-semibold text-white shadow">
                  You are in control · agent paused
                </span>
              </motion.div>
            )}
          </AnimatePresence>

          {viewing && (
            <span className="absolute top-2 right-2 inline-flex items-center gap-1 rounded-full bg-foreground/80 px-2 py-0.5 text-[10px] font-medium text-background backdrop-blur">
              <Camera className="size-3" aria-hidden />
              Step {pickedStep.id} snapshot
            </span>
          )}
        </div>

        {/* Log / history */}
        <div className="border-t">
          <div className="flex items-center justify-between gap-2 px-3 pt-2">
            <div className="flex items-center gap-2 text-xs" aria-live="polite">
              <StatusDot status={status} reduce={reduce} />
              <span className="font-medium">
                {status === "done" ? "Task complete" : status === "paused" ? (human ? "Waiting for you" : "Needs you · 2FA code") : "Agent browsing"}
              </span>
            </div>
            <div className="inline-flex rounded-md border bg-muted/50 p-0.5" role="tablist">
              {(["live", "steps"] as const).map((k) => (
                <button
                  key={k}
                  type="button"
                  role="tab"
                  aria-selected={tab === k}
                  onClick={() => {
                    setTouched(true);
                    setTab(k);
                  }}
                  className={cn(
                    "relative h-5 cursor-pointer rounded px-2 text-[10px] font-medium capitalize outline-none focus-visible:ring-2 focus-visible:ring-ring",
                    tab === k ? "text-foreground" : "text-muted-foreground hover:text-foreground",
                  )}
                >
                  {tab === k && <motion.span layoutId={`${uid}-tab`} className="absolute inset-0 rounded bg-background shadow-xs" transition={{ type: "spring", stiffness: 500, damping: 36 }} />}
                  <span className="relative">{k}</span>
                </button>
              ))}
            </div>
          </div>
          {tab === "live" ? (
            <ol className="h-[66px] space-y-0.5 overflow-hidden px-3 pt-1.5 pb-2 font-mono text-[10.5px]">
              <AnimatePresence initial={false} mode="popLayout">
                {lastLog.map((s, i) => (
                  <motion.li
                    key={s.id}
                    layout={!reduce}
                    initial={reduce ? false : { opacity: 0, y: 10 }}
                    animate={{ opacity: i === lastLog.length - 1 ? 1 : 0.55, y: 0 }}
                    exit={reduce ? undefined : { opacity: 0, y: -10 }}
                    transition={{ duration: 0.3, ease: EASE }}
                    className="flex items-center gap-2 truncate"
                  >
                    <span className="text-muted-foreground">{String(steps.indexOf(s) + 1).padStart(2, "0")}</span>
                    <span className={cn("truncate", s.action === "extract" && "font-semibold text-emerald-700 dark:text-emerald-400")}>{s.label}</span>
                  </motion.li>
                ))}
              </AnimatePresence>
            </ol>
          ) : (
            <div className="flex h-[66px] gap-1.5 overflow-x-auto px-3 pt-1.5 pb-2 [scrollbar-width:none]">
              {steps.map((s) => {
                const on = pickedStep?.id === s.id;
                return (
                  <button
                    key={s.id}
                    type="button"
                    onClick={() => setPicked(s.id)}
                    className={cn(
                      "flex w-24 shrink-0 cursor-pointer flex-col justify-between rounded-lg border p-1.5 text-left outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring",
                      on ? "border-foreground/40 bg-muted" : "hover:bg-muted/60",
                    )}
                  >
                    <span className="font-mono text-[9px] text-muted-foreground uppercase">
                      {s.id} · {s.action}
                    </span>
                    <span className="line-clamp-2 text-[10px] leading-tight">{s.label}</span>
                  </button>
                );
              })}
            </div>
          )}
        </div>
      </div>
      <style>{`
        .ba-ripple { animation: ba-ripple 0.65s cubic-bezier(0.22, 1, 0.36, 1) forwards; }
        @keyframes ba-ripple { from { transform: scale(0.3); opacity: 1; } to { transform: scale(2.4); opacity: 0; } }
      `}</style>
    </div>
  );
}

function StatusDot({ status, reduce }: { status: "running" | "paused" | "done"; reduce: boolean }) {
  if (status === "done")
    return (
      <span className="grid size-4 place-items-center rounded-full bg-emerald-500 text-white">
        <Check className="size-2.5" strokeWidth={3} aria-hidden />
      </span>
    );
  const color = status === "paused" ? "bg-amber-500" : "bg-sky-500";
  return (
    <span className="relative grid size-4 place-items-center">
      {!reduce && <span className={cn("absolute size-2.5 animate-ping rounded-full opacity-60", color)} />}
      <span className={cn("relative size-2 rounded-full", color)} />
    </span>
  );
}

const ORDERS = [
  ["#10482", "Fern & Pine", "$1,240.00", "Paid"],
  ["#10481", "Harbor Labs", "$318.40", "Paid"],
  ["#10480", "Quill Studio", "$2,075.00", "Pending"],
  ["#10479", "Mesa Coffee", "$96.20", "Paid"],
  ["#10478", "Atlas Freight", "$5,410.00", "Refunded"],
];

function DemoPage({ scene, reduce }: { scene: Scene; reduce: boolean }) {
  return (
    <AnimatePresence mode="wait" initial={false}>
      <motion.div
        key={scene.page}
        initial={reduce ? false : { opacity: 0, y: 8 }}
        animate={{ opacity: scene.loading !== null && scene.loading < 0.5 ? 0.4 : 1, y: 0 }}
        exit={reduce ? undefined : { opacity: 0 }}
        transition={{ duration: 0.3, ease: EASE }}
        className="absolute inset-0 bg-gradient-to-b from-muted/40 to-background"
      >
        <div className="flex h-[16%] items-center justify-between border-b bg-background/80 px-3">
          <span className="flex items-center gap-1.5 text-[11px] font-semibold">
            <span className="size-3 rounded-[4px] bg-foreground" />
            Northwind
          </span>
          <span className="flex gap-2" aria-hidden>
            <span className="h-1.5 w-8 rounded bg-foreground/10" />
            <span className="h-1.5 w-8 rounded bg-foreground/10" />
          </span>
        </div>
        {scene.page === "login" && (
          <>
            <p className="absolute top-[21%] w-full text-center text-[12px] font-semibold">Sign in to your portal</p>
            <div className="absolute flex items-center rounded-md border bg-background px-2 text-[10px]" style={{ left: "30%", top: "37%", width: "40%", height: "13%" }}>
              {scene.email ? <span className="font-mono">{scene.email}</span> : <span className="text-muted-foreground">Email</span>}
            </div>
            <div className="absolute flex items-center rounded-md border bg-background px-2 text-[10px] text-muted-foreground" style={{ left: "30%", top: "53%", width: "40%", height: "13%" }}>
              ••••••••••
            </div>
            <div className="absolute grid place-items-center rounded-md bg-foreground text-[10px] font-medium text-background" style={{ left: "30%", top: "70%", width: "40%", height: "13%" }}>
              Sign in
            </div>
          </>
        )}
        {scene.page === "2fa" && (
          <div className="absolute inset-x-[18%] top-[24%] rounded-xl border bg-background p-3 text-center shadow-sm">
            <ShieldAlert className="mx-auto size-4 text-amber-500" aria-hidden />
            <p className="mt-1 text-[11px] font-semibold">Two-step verification</p>
            <p className="text-[9.5px] text-muted-foreground">Enter the code from your authenticator</p>
            <div className="mt-2 flex justify-center gap-1">
              {Array.from({ length: 6 }, (_, i) => (
                <span key={i} className={cn("grid size-5 place-items-center rounded border text-[10px] transition-colors", i < scene.code ? "border-foreground/40 bg-muted" : "")}>
                  {i < scene.code ? "•" : ""}
                </span>
              ))}
            </div>
          </div>
        )}
        {scene.page === "orders" && (
          <div className="absolute inset-x-[4%] top-[22%]">
            <div className="mb-1 flex items-center justify-between px-1 text-[11px] font-semibold">
              Orders
              {scene.extracted && (
                <motion.span
                  initial={reduce ? false : { opacity: 0, scale: 0.7 }}
                  animate={{ opacity: 1, scale: 1 }}
                  className="inline-flex items-center gap-1 rounded-full bg-emerald-500/15 px-1.5 text-[9px] font-medium text-emerald-700 dark:text-emerald-300"
                >
                  <Check className="size-2.5" aria-hidden />
                  12 rows extracted
                </motion.span>
              )}
            </div>
            <div className="divide-y overflow-hidden rounded-md border bg-background text-[9.5px]">
              {ORDERS.map((r, i) => (
                <div
                  key={r[0]}
                  className={cn(
                    "grid grid-cols-[52px_1fr_64px_50px] items-center px-2 py-[3px] transition-colors duration-200",
                    scene.scan === i && "bg-sky-500/15",
                    scene.extracted && "bg-emerald-500/[0.06]",
                  )}
                >
                  <span className="font-mono text-muted-foreground">{r[0]}</span>
                  <span className="truncate">{r[1]}</span>
                  <span className="text-right font-mono tabular-nums">{r[2]}</span>
                  <span className="text-right text-muted-foreground">{r[3]}</span>
                </div>
              ))}
            </div>
          </div>
        )}
      </motion.div>
    </AnimatePresence>
  );
}

export default function BrowserAgentDemo() {
  return (
    <div className="flex h-[380px] w-full items-start justify-center pt-2">
      <BrowserAgent autoPlay />
    </div>
  );
}
Sign in