Components
Auto-scrolling chat container that follows streaming output, stops following the moment the user scrolls up, and offers a jump-to-latest control. Respects prefers-reduced-motion.

"use client";
import { useEffect, useState } from "react";
import Component from "@/components/ui/ai-conversation";
type Turn = {
from: "assistant" | "user";
kind?: "code" | "text" | "tool";
text: string;
};
const TURNS: Turn[] = [
{ from: "user", text: "Why does my dialog feel sluggish?" },
{
from: "assistant",
text: "It animates for 600ms on an ease-in-out curve, so the first third of the motion barely moves. The eye reads that as lag before it reads it as animation.",
},
{ from: "user", text: "What should I use instead?" },
{
from: "assistant",
kind: "tool",
text: "read packages/ui/dialog.tsx · 84 lines",
},
{
from: "assistant",
kind: "code",
text: 'transition={{ type: "spring", duration: 0.25, bounce: 0.1 }}',
},
{
from: "assistant",
text: "A 0.25s spring with a low bounce. It reads as instant while still animating, and it stays interruptible if the user closes early.",
},
{ from: "user", text: "Does that hold up with reduced motion on?" },
{
from: "assistant",
text: "The component checks prefers-reduced-motion and drops to a zero-duration opacity swap, so the dialog still appears — just without the travel.",
},
];
const START_AT = 4;
const STEP_MS = 1600;
export default function DemoOne() {
const [count, setCount] = useState(START_AT);
useEffect(() => {
const id = setInterval(
() =>
setCount((current) =>
current >= TURNS.length ? START_AT : current + 1
),
STEP_MS
);
return () => clearInterval(id);
}, []);
return (
<div className="flex min-h-[700px] w-full items-center justify-center bg-background p-8">
<div className="flex h-[600px] w-full max-w-2xl flex-col overflow-hidden rounded-3xl border bg-card shadow-sm">
<header className="flex items-center gap-3 border-b px-5 py-4">
<span className="flex size-8 items-center justify-center rounded-full bg-foreground font-semibold text-[11px] text-background">
AI
</span>
<div className="min-w-0 flex-1">
<p className="truncate font-semibold text-foreground text-sm">
Motion review
</p>
<p className="text-muted-foreground text-xs">
{count === TURNS.length ? "idle" : "streaming…"} ·{" "}
{TURNS.length} messages
</p>
</div>
<span className="rounded-full border px-2.5 py-1 text-muted-foreground text-xs">
auto-scroll
</span>
</header>
<Component className="flex-1" contentKey={count}>
<div className="space-y-4 p-5">
{TURNS.slice(0, count).map((turn) => {
if (turn.kind === "tool") {
return (
<div
className="flex items-center gap-2 rounded-xl border border-dashed px-3 py-2 text-muted-foreground text-xs"
key={turn.text}
>
<span className="size-1.5 rounded-full bg-muted-foreground" />
{turn.text}
</div>
);
}
if (turn.kind === "code") {
return (
<pre
className="overflow-x-auto rounded-xl bg-muted px-4 py-3 font-mono text-foreground text-xs leading-relaxed"
key={turn.text}
>
{turn.text}
</pre>
);
}
return (
<div
className={
turn.from === "user"
? "ml-auto max-w-[78%] rounded-2xl rounded-br-md bg-foreground px-4 py-2.5 text-background text-sm leading-relaxed"
: "max-w-[82%] rounded-2xl rounded-bl-md border px-4 py-2.5 text-foreground text-sm leading-relaxed"
}
key={turn.text}
>
{turn.text}
</div>
);
})}
</div>
</Component>
<footer className="flex items-center gap-3 border-t px-5 py-4">
<p className="flex-1 rounded-full border px-4 py-2.5 text-muted-foreground text-sm">
Reply to the assistant…
</p>
<span className="flex size-9 items-center justify-center rounded-full bg-foreground text-background text-sm">
↑
</span>
</footer>
</div>
</div>
);
}