shadimage
000h-cojeev
uiagent-statesource: ok · hit

Agent state

A sculpted, living status field that changes contour and color as an agent thinks, works, asks for input, or finishes.

npx shadcn add "https://000h.cojeev.com/r/agent-state.json"
free

Where it goes

stats

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion

Registry dependencies

https://cojeev.com/ui/r/cojeev.json

Files2 · source cached 10/9/2026, 8:12:46 AM

registry/cojeev/ui/agent-state.tsx
"use client";

import * as React from "react";
import { motion, useInView } from "motion/react";
import { cn } from "@/lib/utils";
import { useChoreography } from "@/lib/cojeev-motion/choreography";

export type AgentStatus = "idle" | "thinking" | "working" | "needs-input" | "complete" | "error";
export type AgentStateProps = Omit<React.ComponentProps<"div">, "children"> & {
  status?: AgentStatus;
  size?: "sm" | "md" | "lg";
  label?: string;
  description?: string;
};

const stateLabels: Record<AgentStatus, string> = {
  idle: "Ready when you are", thinking: "Thinking", working: "Working",
  "needs-input": "Your decision", complete: "Complete", error: "Needs attention",
};

// Equal cubic topology lets Motion interpolate a folded fluid silhouette without
// a second animation clock. Each state remains recognizable when motion is off.
const silhouettes: Record<AgentStatus, string> = {
  idle: "M90 24C116 17 151 32 153 60C157 84 139 96 124 116C111 135 81 141 59 126C34 113 22 85 32 61C43 36 65 31 90 24Z",
  thinking: "M90 18C120 27 132 19 148 51C163 78 141 92 135 116C127 146 94 137 68 129C40 123 18 98 30 70C40 48 66 7 90 18Z",
  working: "M90 17C114 11 156 41 151 65C146 85 165 110 134 123C106 140 85 121 61 131C34 140 17 94 30 71C44 47 68 25 90 17Z",
  "needs-input": "M90 27C121 20 147 29 145 61C143 82 162 111 134 125C108 138 91 114 65 127C35 140 28 101 31 76C34 48 62 33 90 27Z",
  complete: "M90 24C118 24 145 36 150 63C155 91 134 116 112 125C87 137 63 128 47 113C29 95 25 69 40 47C53 28 66 24 90 24Z",
  error: "M90 30C113 7 131 35 147 54C167 77 135 93 139 117C142 142 105 128 83 132C55 139 18 117 28 89C37 65 60 40 90 30Z",
};

export function AgentState({ status = "idle", size = "md", label, description, className, ...props }: AgentStateProps) {
  const id = React.useId().replace(/:/g, "");
  const ref = React.useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { margin: "40px" });
  const { quiet, transition } = useChoreography();
  const [visible, setVisible] = React.useState(true);
  React.useEffect(() => {
    const update = () => setVisible(document.visibilityState !== "hidden");
    update();
    document.addEventListener("visibilitychange", update);
    return () => document.removeEventListener("visibilitychange", update);
  }, []);
  const active = !quiet && inView && visible && (status === "thinking" || status === "working");
  const contour = silhouettes[status];
  const alternate = silhouettes[status === "thinking" ? "working" : "thinking"];
  const pathAnimation = active ? { d: [contour, alternate, contour] } : { d: contour };
  const fieldTransition = active ? { duration: status === "working" ? 5 : 7, repeat: Infinity, ease: "easeInOut" as const } : transition;
  return (
    <div ref={ref} data-slot="agent-state" data-status={status} data-size={size} data-animated={active || undefined} className={cn("v-agent-state", className)} role="status" aria-live="polite" aria-atomic="true" {...props}>
      <svg className="v-agent-state__field" viewBox="0 0 180 160" fill="none" aria-hidden="true" focusable="false">
        <defs>
          <radialGradient id={`${id}-body`} cx=".3" cy=".2" r=".85">
            <stop stopColor="var(--agent-light)" />
            <stop offset=".5" stopColor="var(--agent-base)" />
            <stop offset="1" stopColor="var(--agent-deep)" />
          </radialGradient>
          <radialGradient id={`${id}-fold`} cx=".65" cy=".22" r=".8">
            <stop stopColor="var(--cream-fixed)" stopOpacity=".84" />
            <stop offset=".4" stopColor="var(--agent-light)" stopOpacity=".7" />
            <stop offset="1" stopColor="var(--agent-deep)" stopOpacity=".2" />
          </radialGradient>
          <linearGradient id={`${id}-rim`} x1="30" y1="30" x2="138" y2="134" gradientUnits="userSpaceOnUse">
            <stop stopColor="var(--cream-fixed)" stopOpacity=".92" />
            <stop offset=".48" stopColor="var(--agent-light)" stopOpacity=".2" />
            <stop offset="1" stopColor="var(--agent-deep)" stopOpacity=".8" />
          </linearGradient>
          <clipPath id={`${id}-clip`}>
            <motion.path initial={false} d={contour} animate={pathAnimation} transition={fieldTransition} />
          </clipPath>
        </defs>
        <ellipse cx="92" cy="139" rx="43" ry="5" fill="var(--agent-deep)" opacity=".12" />
        <motion.path initial={false} d={contour} animate={pathAnimation} transition={fieldTransition} fill={`url(#${id}-body)`} />
        <g clipPath={`url(#${id}-clip)`}>
          <motion.g initial={false} animate={active ? { rotate: [0, 12, -6, 0], x: [0, 5, -3, 0] } : { rotate: 0, x: 0 }} transition={active ? { duration: 10, repeat: Infinity, ease: "easeInOut" } : transition} style={{ transformOrigin: "90px 80px" }}>
            <path d="M15 61C51 32 81 110 128 89C165 72 142 24 105 14C163 7 181 113 125 142C74 168 20 125 15 61Z" fill={`url(#${id}-fold)`} />
            <path d="M26 48C64 18 59 134 119 110C147 99 132 71 155 47" stroke={`url(#${id}-rim)`} strokeWidth="1.2" />
            <path d="M30 44C69 19 58 139 125 111C151 99 136 69 159 43" stroke={`url(#${id}-rim)`} strokeWidth=".65" opacity=".75" />
            <path d="M35 42C74 26 64 143 131 109C154 97 141 65 162 40" stroke={`url(#${id}-rim)`} strokeWidth=".5" opacity=".5" />
            <path d="M24 92C47 78 93 148 139 118" stroke="var(--agent-deep)" strokeOpacity=".25" strokeWidth="14" />
          </motion.g>
        </g>
        <motion.path initial={false} d={contour} animate={pathAnimation} transition={fieldTransition} stroke={`url(#${id}-rim)`} strokeWidth="1.2" />
        <g className="v-agent-state__mark" stroke="var(--ink-fixed)" strokeWidth="2.3" strokeLinecap="round" strokeLinejoin="round">
          {status === "complete" ? <path d="m77 79 9 9 19-21" /> : status === "error" ? <><path d="M90 66v16" /><path d="M90 91h.01" /></> : status === "needs-input" ? <><path d="M84 72a7 7 0 0 1 14 0c0 5-8 6-8 11" /><path d="M90 92h.01" /></> : <><path d="M77 77v5M101 77v5" /><path d={status === "idle" ? "M84 91q6 4 12 0" : "M85 91h10"} /></>}
        </g>
      </svg>
      <div className="v-agent-state__copy"><span className="v-agent-state__label">{label ?? stateLabels[status]}</span>{description && <span className="v-agent-state__description">{description}</span>}</div>
    </div>
  );
}
registry/cojeev/styles/agent-state.css
@layer cojeev-states {
[data-slot="agent-state"] {
  --agent-base: var(--v-pink);
  --agent-light: var(--v-yellow);
  --agent-deep: var(--v-pink-deep);
  display: flex;
  align-items: center;
  min-width: 0;
  gap: var(--s-3);
  color: var(--v-text);
}
[data-slot="agent-state"][data-status="thinking"] { --agent-base: var(--v-blue); --agent-light: var(--v-pink); --agent-deep: var(--v-blue-deep); }
[data-slot="agent-state"][data-status="working"] { --agent-base: var(--v-pink); --agent-light: var(--v-yellow); --agent-deep: var(--v-pink-deep); }
[data-slot="agent-state"][data-status="needs-input"] { --agent-base: var(--v-yellow); --agent-light: var(--cream-fixed); --agent-deep: var(--v-yellow-deep); }
[data-slot="agent-state"][data-status="complete"] { --agent-base: var(--v-olive); --agent-light: var(--v-yellow); --agent-deep: var(--v-olive-deep); }
[data-slot="agent-state"][data-status="error"] { --agent-base: var(--v-pink); --agent-light: var(--v-yellow); --agent-deep: var(--v-danger); }
.v-agent-state__field { display: block; width: 88px; height: auto; flex: 0 0 auto; overflow: visible; pointer-events: none; }
.v-agent-state__copy { display: grid; min-width: 0; gap: var(--s-1); }
.v-agent-state__label { font: var(--fw-semibold) var(--fs-lead)/1.25 var(--font-display); letter-spacing: -.02em; overflow-wrap: anywhere; }
.v-agent-state__description { font: var(--fw-regular) var(--fs-small)/1.45 var(--font-text); color: var(--v-text-2); max-width: 35ch; overflow-wrap: anywhere; }
[data-slot="agent-state"][data-size="sm"] { gap: var(--s-1); }
[data-slot="agent-state"][data-size="sm"] .v-agent-state__field { width: 48px; }
[data-slot="agent-state"][data-size="sm"] .v-agent-state__label { font-family: var(--font-text); font-size: var(--fs-small); letter-spacing: 0; }
[data-slot="agent-state"][data-size="sm"] .v-agent-state__description { font-size: var(--fs-meta); }
[data-slot="agent-state"][data-size="lg"] { flex-direction: column; text-align: center; gap: 0; }
[data-slot="agent-state"][data-size="lg"] .v-agent-state__field { width: min(100%, 216px); }
[data-slot="agent-state"][data-size="lg"] .v-agent-state__label { font-size: var(--fs-section); }
[data-slot="agent-state"][data-size="lg"] .v-agent-state__description { max-width: 32ch; margin-inline: auto; font-size: var(--fs-control); }
@media (forced-colors: active) {
  .v-agent-state__field { forced-color-adjust: none; }
}

}

More from 000h-cojeev

Browse all components in this library →