A small pill that says what an assistant is busy with. The status shimmers dark to light, and each new one slides in over the last.
pnpm dlx shadcn@latest add @latheui/thinking-pillFirst time? Run npx latheui init once to add the @latheui registry. The CLI then writes the source into your project and installs what it needs. Or add it by hand:
Install the npm dependencies.
pnpm add motionAdd the Lathe items this component imports.
lib/motion.tsCopy the source into your project.
"use client"
import * as React from "react"
import { AnimatePresence, motion } from "motion/react"
import { cn } from "@/lib/utils"
import { duration, ease, useMotionSafe } from "@/lib/motion"
import { TextShimmer } from "@/components/ui/text-shimmer"
export type ThinkingPillProps = Omit<React.ComponentProps<"div">, "children"> & {
/** What the assistant is doing right now, for example "Searching the web". */
status: string
/** Replaces the spinner, for example with a tool's icon. */
icon?: React.ReactNode
/** Seconds per pass of the highlight across the text. */
shimmerDuration?: number
}
/**
* A small pill that says what an assistant is busy with. The status shimmers from dark to
* light, and a new status slides in over the old one.
*/
function ThinkingPill({ status, icon, shimmerDuration = 1.8, className, ...props }: ThinkingPillProps) {
const { reduced, safe } = useMotionSafe()
return (
<div
data-slot="thinking-pill"
role="status"
aria-live="polite"
className={cn(
"inline-flex h-8 max-w-full items-center gap-2 overflow-hidden rounded-full border border-border bg-card pr-3.5 pl-2.5 text-[13px] shadow-[0_1px_2px_rgb(0_0_0/0.05)]",
className
)}
{...props}
>
<span aria-hidden="true" className="grid size-4 shrink-0 place-items-center text-muted-foreground [&_svg]:size-3.5">
{icon ?? <Spinner />}
</span>
<span className="relative grid min-w-0">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={status}
initial={reduced ? { opacity: 0 } : { opacity: 0, y: 10, filter: "blur(2px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
exit={reduced ? { opacity: 0 } : { opacity: 0, y: -10, filter: "blur(2px)" }}
transition={safe({ duration: duration.slow, ease: ease.out })}
className="truncate whitespace-nowrap"
>
{/* Darker text with a lighter highlight travelling left to right. */}
<TextShimmer duration={shimmerDuration} className="font-medium">
{status}
</TextShimmer>
</motion.span>
</AnimatePresence>
</span>
</div>
)
}
function Spinner() {
return (
<svg viewBox="0 0 16 16" fill="none" className="motion-safe:animate-spin">
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.75" opacity="0.2" />
<path d="M14 8a6 6 0 0 0-6-6" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" />
</svg>
)
}
export { ThinkingPill }Depends on
Also accepts native div attributes.
| Prop | Type | Default | Description |
|---|---|---|---|
| status* (required) | string | — | What the assistant is doing, such as “Searching the web”. |
| icon | ReactNode | — | Shown in place of the spinner. Decorative; the status carries the meaning. |
| shimmerDuration | number | 1.8 | Seconds per pass of the highlight. |
<ThinkingPill
status="Reading sources"
className="h-10 bg-zinc-900 px-4 text-sm text-zinc-100"
/>