# Bento AI Capabilities — React Bento Grid

> An AI feature bento: an answer streams word by word with citations, an agent ticks through its steps, a voice waveform pulses and a reply cross-fades between languages.

- Category: [Bento Grids](https://pixelpapa.com/components/bento.md)
- Tags: bento, ai, streaming, agent, grid
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/bento/bento-ai-capabilities

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/bento-ai-capabilities.json"
```

The file is written to `components/pixelpapa/bento-ai-capabilities.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { Check, LoaderCircle, Mic, Sparkles } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useEffect, useState, type ReactNode } from "react";
import { cn } from "@/lib/utils";

const ease = [0.22, 1, 0.36, 1] as const;

/** Counts 0…max on an interval, rests for `rest` ticks at the end, then loops. */
function useLoop(max: number, ms: number, rest: number) {
  const reduce = useReducedMotion();
  const [tick, setTick] = useState(0);
  useEffect(() => {
    if (reduce) return;
    const id = setInterval(() => setTick((t) => (t + 1) % (max + rest)), ms);
    return () => clearInterval(id);
  }, [reduce, max, ms, rest]);
  return reduce ? max : Math.min(tick, max);
}

function Tile({ className, children }: { className?: string; children: ReactNode }) {
  return (
    <motion.div
      variants={{ hidden: { opacity: 0, y: 24, scale: 0.98 }, shown: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.7, ease } } }}
      className={cn("relative flex flex-col overflow-hidden rounded-3xl border border-border bg-card p-6", className)}
    >
      <div aria-hidden className="absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-primary/60 to-transparent" />
      {children}
    </motion.div>
  );
}

function Caption({ title, body }: { title: string; body: string }) {
  return (
    <div className="mt-auto pt-6">
      <h3 className="font-medium text-card-foreground">{title}</h3>
      <p className="mt-1 text-sm text-muted-foreground">{body}</p>
    </div>
  );
}

const answer =
  "Yes. Your Kestrel Pro plan includes two free returns per order. I have emailed a prepaid label for order 4821, and the refund of $148.00 will reach your card within three business days of the scan.".split(" ");

function Streaming() {
  const shown = useLoop(answer.length, 70, 45);
  const done = shown >= answer.length;
  return (
    <div className="space-y-3 text-sm">
      <p className="ml-auto w-fit max-w-[80%] rounded-2xl rounded-br-md bg-muted px-4 py-2.5 text-foreground">
        Can I return the boots I ordered last week?
      </p>
      <div className="flex gap-3">
        <span className="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
          <Sparkles className="size-3.5" />
        </span>
        <div className="min-h-28">
          <p className="leading-relaxed text-card-foreground">
            {answer.slice(0, shown).join(" ")}
            {!done && <span className="ml-1 inline-block size-2 animate-pulse rounded-full bg-primary align-middle" />}
          </p>
          <div className={cn("mt-3 flex flex-wrap gap-2 transition-all duration-500", done ? "opacity-100" : "translate-y-1 opacity-0")}>
            {["Returns policy §2", "Order 4821", "Plan: Kestrel Pro"].map((c, i) => (
              <span key={c} className="rounded-full border border-border bg-background px-2.5 py-1 text-xs text-muted-foreground">
                <span className="mr-1 font-mono text-primary">{i + 1}</span>
                {c}
              </span>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

const agentSteps = ["Read the ticket and 4 past replies", "Looked up order 4821", "Checked returns policy", "Issued prepaid label", "Drafted reply for review"];

function Agent() {
  const at = useLoop(agentSteps.length, 900, 3);
  return (
    <ol className="relative space-y-4">
      <span aria-hidden className="absolute top-3 bottom-3 left-[11px] w-px bg-border" />
      {agentSteps.map((s, i) => (
        <li key={s} className="relative flex items-center gap-3 text-sm">
          <span
            className={cn(
              "flex size-6 shrink-0 items-center justify-center rounded-full border transition-colors duration-300",
              i < at ? "border-primary bg-primary text-primary-foreground" : "border-border bg-card text-muted-foreground",
            )}
          >
            {i < at ? <Check className="size-3.5" strokeWidth={3} /> : i === at ? <LoaderCircle className="size-3.5 animate-spin" /> : null}
          </span>
          <span className={cn("transition-colors duration-300", i <= at ? "text-card-foreground" : "text-muted-foreground/60")}>{s}</span>
        </li>
      ))}
    </ol>
  );
}

const bars = [0.35, 0.6, 0.9, 0.5, 0.75, 1, 0.55, 0.8, 0.4, 0.7, 0.95, 0.5, 0.65, 0.85, 0.45, 0.6, 0.3, 0.7, 0.5, 0.4];

function Voice() {
  return (
    <div className="flex items-center gap-4">
      <span className="flex size-11 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
        <Mic className="size-5" />
      </span>
      <div className="flex h-14 flex-1 items-center gap-[3px]">
        {bars.map((b, i) => (
          <span
            key={i}
            className="flex-1 rounded-full bg-primary motion-reduce:animate-none"
            style={{
              height: `${b * 100}%`,
              opacity: 0.35 + b * 0.65,
              animation: `pp-bento-ai-capabilities-wave ${0.9 + (i % 5) * 0.14}s ease-in-out ${-(i % 7) * 0.13}s infinite alternate`,
            }}
          />
        ))}
      </div>
    </div>
  );
}

const replies = [
  ["English", "Your refund is on its way."],
  ["Deutsch", "Ihre Rückerstattung ist unterwegs."],
  ["Español", "Tu reembolso está en camino."],
  ["Français", "Votre remboursement est en route."],
];

function Translate() {
  const at = useLoop(replies.length * 4 - 1, 600, 1);
  const [lang, text] = replies[Math.floor(at / 4) % replies.length];
  return (
    <div>
      <div className="flex gap-1.5">
        {replies.map(([l]) => (
          <span
            key={l}
            className={cn("rounded-full px-2.5 py-1 text-xs transition-colors duration-300", l === lang ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground")}
          >
            {l.slice(0, 2).toUpperCase()}
          </span>
        ))}
      </div>
      <div className="relative mt-4 h-14">
        <AnimatePresence mode="wait" initial={false}>
          <motion.p
            key={lang}
            initial={{ opacity: 0, y: 8, filter: "blur(6px)" }}
            animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
            exit={{ opacity: 0, y: -8, filter: "blur(6px)" }}
            transition={{ duration: 0.3 }}
            className="absolute inset-0 font-serif text-xl leading-snug text-card-foreground"
          >
            {text}
          </motion.p>
        </AnimatePresence>
      </div>
    </div>
  );
}

export default function BentoAiCapabilities() {
  return (
    <section className="w-full bg-background px-6 py-20 sm:py-24">
      <style>{`@keyframes pp-bento-ai-capabilities-wave { from { transform: scaleY(0.3); } to { transform: scaleY(1); } }`}</style>
      <div className="mx-auto max-w-6xl">
        <div className="mx-auto max-w-2xl text-center">
          <p className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
            <Sparkles className="size-3.5" /> Solace Assist
          </p>
          <h2 className="mt-4 text-4xl font-semibold tracking-tight text-balance text-foreground sm:text-5xl">
            A support teammate that reads the whole thread first
          </h2>
        </div>
        <motion.div
          initial="hidden"
          whileInView="shown"
          viewport={{ once: true, amount: 0.15 }}
          transition={{ staggerChildren: 0.08 }}
          className="mt-12 grid gap-4 md:grid-cols-3"
        >
          <Tile className="md:col-span-2">
            <Streaming />
            <Caption title="Answers grounded in your policies" body="Every claim links to the document or order it came from." />
          </Tile>
          <Tile className="md:row-span-2">
            <p className="mb-5 text-xs font-medium tracking-wide text-muted-foreground uppercase">Agent run · 6.2s</p>
            <Agent />
            <Caption title="Takes the action, not just the note" body="Refunds, labels and plan changes run through the tools you already approved." />
          </Tile>
          <Tile>
            <Voice />
            <Caption title="Voice in 180 ms" body="Low-latency speech for phone queues, with live transcripts." />
          </Tile>
          <Tile>
            <Translate />
            <Caption title="Fluent in 42 languages" body="Replies in the customer's language, in your tone." />
          </Tile>
        </motion.div>
        <p className="mt-8 text-center text-sm text-muted-foreground">
          <span className="font-medium text-foreground">68%</span> of conversations resolved without a handoff across 310 support teams
        </p>
      </div>
    </section>
  );
}
```

## AI prompt

```text
Add the "Bento AI Capabilities" component from PixelPapa UI to this project.

What it is: An AI feature bento: an answer streams word by word with citations, an agent ticks through its steps, a voice waveform pulses and a reply cross-fades between languages.
Category: Bento Grids. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/bento-ai-capabilities.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/bento-ai-capabilities.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/bento-ai-capabilities" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More bento grids

- [Bento Analytics](https://pixelpapa.com/components/bento/bento-analytics.md) (Pro): A metrics bento: an MRR curve that morphs between ranges, a donut whose legend isolates segments, a ticking live-visitor count, weekday bars and a row of sparklines.
- [Bento Collaboration](https://pixelpapa.com/components/bento/bento-collaboration.md): A multiplayer bento: named cursors glide across a shared board, comments pop into a thread, a teammate types live and reactions float up from a decision.
- [Bento Developer](https://pixelpapa.com/components/bento/bento-developer.md) (Pro): A developer bento: a terminal replays a deploy line by line, an API response swaps with the selected endpoint, a p95 latency graph scrolls live and uptime bars fill in.
- [Bento Hover Expand](https://pixelpapa.com/components/bento/bento-hover-expand.md): Four tall tiles share a row; the hovered or focused one springs wide to reveal its detail and metric while its siblings narrow and dim.
- [Bento Product Overview](https://pixelpapa.com/components/bento/bento-product-overview.md) (Pro): Five product tiles that each run a tiny working demo: a scrubbable revenue line, a self-typing command bar, spring switches, a fanning avatar stack and a live timer.
- [Bento Security Globe](https://pixelpapa.com/components/bento/bento-security-globe.md) (Pro): A security bento around a slowly rotating dotted SVG globe with region pins and traffic arcs, flanked by a ticking threat counter, a key-rotation ring and a residency picker.
