For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Agent Thinking

An activity indicator for an assistant response.

Installation

$ pnpm dlx shadcn@latest add @cobalt/agent-thinking

Source

"use client";

import { cn } from "cn";
import * as React from "react";

export type AgentThinkingVariant = "wave" | "spin" | "stars" | "infinity";
export type AgentThinkingTone = "subtle" | "default" | "primary" | "accent";
export interface AgentThinkingProps extends React.ComponentProps<"div"> {
  variant?: AgentThinkingVariant;
  label?: string;
  tone?: AgentThinkingTone;
  shimmer?: boolean;
  showTimer?: boolean;
}

const colors: Record<AgentThinkingTone, string> = {
  subtle: "var(--foreground-tertiary, var(--muted-foreground))",
  default: "var(--foreground-tertiary, var(--muted-foreground))",
  primary: "var(--foreground-strong, var(--foreground))",
  accent: "var(--agent-thinking-accent, var(--primary))",
};
const seed = [0.55, 0.3, 0.15, 0.85, 0.55, 0.3, 1, 0.85, 0.55];
const stars = [
  { x: 50, y: 46, scale: 1 },
  { x: 18, y: 22, scale: 0.55 },
  { x: 82, y: 26, scale: 0.45 },
  { x: 78, y: 76, scale: 0.55 },
  { x: 22, y: 78, scale: 0.4 },
];
const infinity =
  "M28 14C33 5 47 5 47 14C47 23 33 23 28 14C23 5 9 5 9 14C9 23 23 23 28 14Z";

function Dots({ variant }: { variant: "wave" | "spin" }) {
  const [opacities, setOpacities] = React.useState(seed);
  React.useEffect(() => {
    const media = window.matchMedia("(prefers-reduced-motion: reduce)");
    let phase = 0;
    const id = window.setInterval(() => {
      if (media.matches) {
        setOpacities(seed);
        return;
      }
      phase = (phase + 1 / 8) % 1;
      setOpacities(
        seed.map((_, i) => {
          const col = i % 3,
            row = Math.floor(i / 3);
          const scalar =
            variant === "wave"
              ? ((col + row) / 4) * 0.75
              : (Math.atan2(row - 1, col - 1) / (2 * Math.PI) + 1) % 1;
          const lit = Math.max(0, 1 - ((phase - scalar + 1) % 1) / 0.3) ** 1.5;
          return 0.12 + 0.88 * lit;
        })
      );
    }, 80);
    return () => window.clearInterval(id);
  }, [variant]);
  return (
    <span
      aria-hidden
      className="grid shrink-0 grid-cols-[repeat(3,4px)] gap-[2px]"
    >
      {opacities.map((opacity, i) => (
        <span
          key={i}
          className="size-[4px] rounded-[1px] bg-current transition-opacity duration-[220ms] ease-[ease] motion-reduce:transition-none"
          style={{ opacity }}
        />
      ))}
    </span>
  );
}
function Indicator({ variant }: { variant: AgentThinkingVariant }) {
  if (variant === "wave" || variant === "spin") {
    return <Dots variant={variant} />;
  }
  if (variant === "stars") {
    return (
      <span
        aria-hidden
        className="agent-thinking-stars relative block size-[21px] shrink-0"
      >
        {stars.map((star, i) => {
          const size = 14 * star.scale;
          return (
            <svg
              key={i}
              viewBox="0 0 24 24"
              className="agent-thinking-star absolute"
              style={{
                width: size,
                height: size,
                left: `${star.x}%`,
                top: `${star.y}%`,
                marginLeft: -size / 2,
                marginTop: -size / 2,
                animationDelay: `${(i * 1.4 * 0.7) / 5}s`,
              }}
            >
              <path
                d="M12 0C13 7 17 11 24 12C17 13 13 17 12 24C11 17 7 13 0 12C7 11 11 7 12 0Z"
                fill="currentColor"
              />
            </svg>
          );
        })}
      </span>
    );
  }
  return (
    <svg
      aria-hidden
      viewBox="0 0 56 28"
      className="-mx-[4px] h-[16px] w-[32px] shrink-0"
    >
      <path
        d={infinity}
        fill="none"
        stroke="currentColor"
        strokeWidth={2.75}
        opacity={0.15}
      />
      <path
        d={infinity}
        pathLength={100}
        fill="none"
        stroke="currentColor"
        strokeWidth={2.75}
        strokeLinecap="round"
        strokeDasharray="11 89"
        className="agent-thinking-comet"
      />
    </svg>
  );
}
function Timer() {
  const [elapsed, setElapsed] = React.useState(0);
  React.useEffect(() => {
    const start = performance.now();
    const id = window.setInterval(
      () => setElapsed((performance.now() - start) / 1000),
      100
    );
    return () => window.clearInterval(id);
  }, []);
  return (
    <span
      aria-hidden
      className="font-mono text-xs leading-4 font-normal tracking-[0.15px] text-[color:var(--foreground-tertiary,var(--muted-foreground))] tabular-nums"
    >
      {elapsed.toFixed(1)}s
    </span>
  );
}
export function AgentThinking({
  variant = "wave",
  label = "Thinking",
  tone,
  shimmer = true,
  showTimer = true,
  className,
  style,
  ...props
}: AgentThinkingProps) {
  const color = colors[tone ?? (variant === "stars" ? "subtle" : "default")];
  return (
    <div
      {...props}
      role="status"
      data-slot="agent-thinking"
      className={cn("flex items-center gap-2.5", className)}
      style={
        {
          ...style,
          color,
          "--agent-thinking-tone": color,
        } as React.CSSProperties
      }
    >
      <Indicator variant={variant} />
      <span
        className={cn(
          "text-sm leading-5 font-medium",
          shimmer && "agent-thinking-label"
        )}
      >
        {label}
      </span>
      {showTimer && <Timer />}
    </div>
  );
}