{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "constellation",
  "type": "registry:component",
  "title": "Constellation",
  "description": "Your agents and the tools they reach for, drawn as a small constellation: each call and its answer travel the line as a spark, busy lines glow brighter, and an agent that needs you turns amber with Approve right underneath.",
  "categories": [
    "component",
    "Mission Control"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/constellation.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { BotIcon, CheckIcon, HandIcon, type LucideIcon, WrenchIcon, XIcon } from \"lucide-react\";\nimport { AnimatePresence, animate, MotionConfig, motion, useMotionValue, useReducedMotion, useTransform } from \"motion/react\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nfunction useReducedMotionAfterMount() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && Boolean(prefers);\n}\n\nexport interface ConstellationAgent {\n  id: string;\n  name: string;\n  icon?: LucideIcon;\n  at?: [number, number];\n  asks?: { tool: string; action: string }[];\n}\n\nexport interface ConstellationTool {\n  id: string;\n  name: string;\n  icon?: LucideIcon;\n  at?: [number, number];\n}\n\nexport interface ConstellationLink {\n  agent: string;\n  tool: string;\n}\n\nexport interface ConstellationProps {\n  agents: ConstellationAgent[];\n  tools: ConstellationTool[];\n  links: ConstellationLink[];\n  title?: string;\n  live?: boolean;\n  autoApproveAfterMs?: number;\n  onDecision?: (ask: ConstellationAsk, approved: boolean) => void;\n  className?: string;\n}\n\nexport interface ConstellationAsk {\n  agent: string;\n  tool: string;\n  action: string;\n}\n\ntype AgentStatus = \"idle\" | \"working\" | \"waiting\";\n\ninterface Pulse {\n  id: number;\n  agent: string;\n  tool: string;\n  dir: \"call\" | \"answer\";\n}\n\ninterface LogLine {\n  id: number;\n  agent: string;\n  tool: string;\n  text: string;\n  tone: \"call\" | \"ok\" | \"warn\" | \"deny\";\n}\n\ninterface Point {\n  x: number;\n  y: number;\n}\n\nconst W = 440;\nconst H = 260;\nconst CX = W / 2;\nconst CY = H / 2;\n\nconst key = (a: string, t: string) => `${a}→${t}`;\n\nfunction ringLayout(agents: ConstellationAgent[], tools: ConstellationTool[], links: ConstellationLink[]) {\n  const pos: Record<string, Point> = {};\n  const angles: Record<string, number> = {};\n  const n = agents.length;\n  agents.forEach((a, i) => {\n    const ang = n === 1 ? -Math.PI / 2 : -Math.PI / 2 + (i / n) * Math.PI * 2;\n    angles[a.id] = ang;\n    pos[a.id] = n === 1 ? { x: CX, y: CY } : { x: CX + Math.cos(ang) * 82, y: CY + Math.sin(ang) * 52 };\n  });\n  const want = tools.map((t) => {\n    const users = links.filter((l) => l.tool === t.id).map((l) => angles[l.agent] ?? 0);\n    const sx = users.reduce((s, a) => s + Math.cos(a), 0);\n    const sy = users.reduce((s, a) => s + Math.sin(a), 0);\n    return { id: t.id, ang: users.length ? Math.atan2(sy, sx) : 0 };\n  });\n  want.sort((a, b) => a.ang - b.ang);\n  const m = want.length;\n  const step = (Math.PI * 2) / Math.max(m, 1);\n  let best = 0;\n  let bestErr = Number.POSITIVE_INFINITY;\n  for (let k = 0; k < 72; k++) {\n    const off = (k / 72) * step;\n    let err = 0;\n    want.forEach((w, i) => {\n      const d = Math.atan2(Math.sin(w.ang - (off + i * step - Math.PI)), Math.cos(w.ang - (off + i * step - Math.PI)));\n      err += d * d;\n    });\n    if (err < bestErr) {\n      bestErr = err;\n      best = off;\n    }\n  }\n  want.forEach((w, i) => {\n    const ang = best + i * step - Math.PI;\n    pos[w.id] = { x: CX + Math.cos(ang) * 176, y: CY + Math.sin(ang) * 104 };\n  });\n  for (const node of [...agents, ...tools]) if (node.at) pos[node.id] = { x: node.at[0] * W, y: node.at[1] * H };\n  return pos;\n}\n\nfunction curve(a: Point, b: Point) {\n  const mx = (a.x + b.x) / 2;\n  const my = (a.y + b.y) / 2;\n  const dx = b.x - a.x;\n  const dy = b.y - a.y;\n  const len = Math.hypot(dx, dy) || 1;\n  const bend = 0.12 * len;\n  const c = { x: mx - (dy / len) * bend, y: my + (dx / len) * bend };\n  return { c, d: `M${a.x} ${a.y} Q${c.x} ${c.y} ${b.x} ${b.y}` };\n}\n\nfunction quad(a: Point, c: Point, b: Point, t: number) {\n  const u = 1 - t;\n  return { x: u * u * a.x + 2 * u * t * c.x + t * t * b.x, y: u * u * a.y + 2 * u * t * c.y + t * t * b.y };\n}\n\nfunction PulseDot({\n  from,\n  to,\n  ctrl,\n  tone,\n  reduce,\n  dim,\n  onDone,\n}: {\n  dim: boolean;\n  from: Point;\n  to: Point;\n  ctrl: Point;\n  tone: \"call\" | \"answer\";\n  reduce: boolean;\n  onDone: () => void;\n}) {\n  const t = useMotionValue(0);\n  const cx = useTransform(t, (v) => quad(from, ctrl, to, v).x);\n  const cy = useTransform(t, (v) => quad(from, ctrl, to, v).y);\n  const done = useRef(onDone);\n  done.current = onDone;\n  useEffect(() => {\n    const c = animate(t, 1, {\n      duration: reduce ? 0.05 : 0.75,\n      ease: [0.45, 0, 0.25, 1],\n      onComplete: () => done.current(),\n    });\n    return () => c.stop();\n  }, [t, reduce]);\n  return (\n    <g className=\"transition-opacity duration-300\" opacity={dim ? 0.15 : 1}>\n      <motion.circle\n        className={tone === \"call\" ? \"fill-info/25\" : \"fill-success/25\"}\n        cx={cx}\n        cy={cy}\n        r={7}\n      />\n      <motion.circle className={tone === \"call\" ? \"fill-info\" : \"fill-success\"} cx={cx} cy={cy} r={3.25} />\n    </g>\n  );\n}\n\nexport function Constellation({\n  agents,\n  tools,\n  links,\n  title = \"Agent network\",\n  live = true,\n  autoApproveAfterMs,\n  onDecision,\n  className,\n}: ConstellationProps) {\n  const reduce = useReducedMotionAfterMount();\n  const pos = useMemo(() => ringLayout(agents, tools, links), [agents, tools, links]);\n  const paths = useMemo(() => {\n    const out: Record<string, { d: string; c: Point; a: Point; b: Point }> = {};\n    for (const l of links) {\n      const a = pos[l.agent];\n      const b = pos[l.tool];\n      if (!a || !b) continue;\n      out[key(l.agent, l.tool)] = { ...curve(a, b), a, b };\n    }\n    return out;\n  }, [links, pos]);\n\n  const [status, setStatus] = useState<Record<string, AgentStatus>>(() =>\n    Object.fromEntries(agents.map((a, i) => [a.id, i === agents.length - 1 ? \"idle\" : \"working\"])),\n  );\n  const [pulses, setPulses] = useState<Pulse[]>([]);\n  const [heat, setHeat] = useState<Record<string, number>>({});\n  const [toolBusy, setToolBusy] = useState<Record<string, number>>({});\n  const [ask, setAsk] = useState<ConstellationAsk | null>(null);\n  const [log, setLog] = useState<LogLine[]>([]);\n  const [calls, setCalls] = useState(0);\n  const [hoverId, setHover] = useState<string | null>(null);\n  const [pinned, setPinned] = useState<string | null>(null);\n  const hover = hoverId ?? pinned;\n\n  const ids = useRef(0);\n  const inFlight = useRef(new Set<string>());\n  const timers = useRef(new Set<ReturnType<typeof setTimeout>>());\n  const statusRef = useRef(status);\n  statusRef.current = status;\n  const askRef = useRef(ask);\n  askRef.current = ask;\n\n  const later = useCallback((fn: () => void, ms: number) => {\n    const id = setTimeout(() => {\n      timers.current.delete(id);\n      fn();\n    }, ms);\n    timers.current.add(id);\n  }, []);\n\n  useEffect(() => {\n    const t = timers.current;\n    return () => {\n      for (const id of t) clearTimeout(id);\n      t.clear();\n    };\n  }, []);\n\n  const pushLog = useCallback((line: Omit<LogLine, \"id\">) => {\n    setLog((l) => [{ ...line, id: ++ids.current }, ...l].slice(0, 3));\n  }, []);\n\n  const fire = useCallback(\n    (agent: string, tool: string) => {\n      inFlight.current.add(agent);\n      setPulses((p) => [...p, { id: ++ids.current, agent, tool, dir: \"call\" }]);\n      setHeat((h) => ({ ...h, [key(agent, tool)]: Math.min((h[key(agent, tool)] ?? 0) + 1, 5) }));\n      setCalls((c) => c + 1);\n    },\n    [],\n  );\n\n  const resolve = useCallback(\n    (approved: boolean) => {\n      const a = askRef.current;\n      if (!a) return;\n      setAsk(null);\n      onDecision?.(a, approved);\n      setStatus((s) => ({ ...s, [a.agent]: \"working\" }));\n      const agentName = agents.find((x) => x.id === a.agent)?.name ?? a.agent;\n      if (approved) {\n        pushLog({ agent: agentName, tool: a.action, text: \"approved\", tone: \"ok\" });\n        fire(a.agent, a.tool);\n      } else {\n        pushLog({ agent: agentName, tool: a.action, text: \"denied\", tone: \"deny\" });\n        inFlight.current.delete(a.agent);\n      }\n    },\n    [agents, fire, onDecision, pushLog],\n  );\n\n  useEffect(() => {\n    if (!live) return;\n    let tick = 0;\n    let nextAsk = 8;\n    const iv = setInterval(() => {\n      tick++;\n      const st = statusRef.current;\n      for (const a of agents) {\n        const s = st[a.id];\n        if (s === \"working\" && !inFlight.current.has(a.id) && Math.random() < 0.5) {\n          const mine = links.filter((l) => l.agent === a.id);\n          const pick = mine[Math.floor(Math.random() * mine.length)];\n          if (pick) fire(a.id, pick.tool);\n        }\n      }\n      if (tick % 9 === 0) {\n        const pool = agents.filter((a) => st[a.id] !== \"waiting\" && !inFlight.current.has(a.id));\n        const flip = pool[Math.floor(Math.random() * pool.length)];\n        const busy = agents.filter((a) => st[a.id] === \"working\").length;\n        if (flip && (st[flip.id] === \"idle\" || busy > 2)) {\n          setStatus((s) => ({ ...s, [flip.id]: s[flip.id] === \"idle\" ? \"working\" : \"idle\" }));\n        }\n      }\n      if (askRef.current) nextAsk = tick + 16;\n      else if (tick >= nextAsk) {\n        const candidates = agents.filter((a) => a.asks?.length && st[a.id] === \"working\" && !inFlight.current.has(a.id));\n        const who = candidates[Math.floor(Math.random() * candidates.length)];\n        if (who?.asks) {\n          const q = who.asks[Math.floor(Math.random() * who.asks.length)];\n          inFlight.current.add(who.id);\n          setStatus((s) => ({ ...s, [who.id]: \"waiting\" }));\n          setAsk({ agent: who.id, tool: q.tool, action: q.action });\n          pushLog({ agent: who.name, tool: q.action, text: \"needs you\", tone: \"warn\" });\n        }\n      }\n      setHeat((h) => {\n        const n: Record<string, number> = {};\n        for (const [k, v] of Object.entries(h)) if (v * 0.88 > 0.05) n[k] = v * 0.88;\n        return n;\n      });\n    }, 450);\n    return () => clearInterval(iv);\n  }, [live, agents, links, fire, pushLog]);\n\n  useEffect(() => {\n    if (!ask || !autoApproveAfterMs) return;\n    const id = setTimeout(() => resolve(true), autoApproveAfterMs);\n    return () => clearTimeout(id);\n  }, [ask, autoApproveAfterMs, resolve]);\n\n  const onPulseDone = useCallback(\n    (p: Pulse) => {\n      setPulses((list) => list.filter((x) => x.id !== p.id));\n      if (p.dir === \"call\") {\n        setToolBusy((b) => ({ ...b, [p.tool]: (b[p.tool] ?? 0) + 1 }));\n        later(\n          () => {\n            setToolBusy((b) => ({ ...b, [p.tool]: Math.max((b[p.tool] ?? 1) - 1, 0) }));\n            setPulses((list) => [...list, { id: ++ids.current, agent: p.agent, tool: p.tool, dir: \"answer\" }]);\n          },\n          250 + Math.random() * 550,\n        );\n      } else {\n        inFlight.current.delete(p.agent);\n        const a = agents.find((x) => x.id === p.agent);\n        const t = tools.find((x) => x.id === p.tool);\n        pushLog({\n          agent: a?.name ?? p.agent,\n          tool: t?.name ?? p.tool,\n          text: `${Math.round(120 + Math.random() * 760)}ms`,\n          tone: \"call\",\n        });\n      }\n    },\n    [agents, tools, later, pushLog],\n  );\n\n  const related = (k: string) => {\n    if (!hover) return true;\n    const [a, t] = k.split(\"→\");\n    return a === hover || t === hover;\n  };\n\n  const working = agents.filter((a) => status[a.id] === \"working\").length;\n  const askAgent = ask ? agents.find((a) => a.id === ask.agent) : null;\n  const askTool = ask ? tools.find((t) => t.id === ask.tool) : null;\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n    <div className={cn(\"w-full max-w-[480px] overflow-hidden rounded-2xl border bg-card shadow-xs/5\", className)}>\n      <div className=\"flex items-center justify-between gap-3 px-4 pt-3.5 pb-1\">\n        <div className=\"flex min-w-0 items-center gap-2\">\n          <span className=\"relative flex size-2\">\n            {live && !reduce && <span className=\"absolute inset-0 animate-ping rounded-full bg-success/60\" />}\n            <span className=\"relative size-2 rounded-full bg-success\" />\n          </span>\n          <h3 className=\"truncate font-medium text-sm\">{title}</h3>\n        </div>\n        <div className=\"flex shrink-0 items-center gap-3 text-muted-foreground text-xs tabular-nums\">\n          <span>\n            <span className=\"text-foreground\">{working}</span>/{agents.length} working\n          </span>\n          <span>\n            <span className=\"text-foreground\">{calls}</span> calls\n          </span>\n        </div>\n      </div>\n\n      <div className=\"relative mx-auto aspect-[440/260] w-full\">\n        <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\" viewBox={`0 0 ${W} ${H}`}>\n          {links.map((l) => {\n            const k = key(l.agent, l.tool);\n            const p = paths[k];\n            if (!p) return null;\n            const h = heat[k] ?? 0;\n            const pending = ask && ask.agent === l.agent && ask.tool === l.tool;\n            return (\n              <g key={k}>\n              <motion.path\n                animate={{\n                  strokeWidth: pending ? 1.5 : 1 + h * 0.9,\n                  opacity: related(k) ? (pending ? 1 : 0.35 + Math.min(h, 3) * 0.2) : 0.12,\n                }}\n                className={cn(pending ? \"stroke-warning\" : \"stroke-foreground/45\")}\n                d={p.d}\n                fill=\"none\"\n                initial={false}\n                strokeDasharray={pending ? \"4 4\" : undefined}\n                strokeLinecap=\"round\"\n                transition={{ type: \"spring\", stiffness: 140, damping: 22 }}\n              />\n              <motion.path\n                animate={{\n                  strokeWidth: 1 + h * 0.9,\n                  opacity: pending || !related(k) ? 0 : Math.min(h / 2.5, 1) * 0.55,\n                }}\n                className=\"stroke-info\"\n                d={p.d}\n                fill=\"none\"\n                initial={false}\n                strokeLinecap=\"round\"\n                transition={{ type: \"spring\", stiffness: 140, damping: 22 }}\n              />\n              </g>\n            );\n          })}\n          {pulses.map((p) => {\n            const path = paths[key(p.agent, p.tool)];\n            if (!path) return null;\n            return (\n              <PulseDot\n                ctrl={path.c}\n                dim={!related(key(p.agent, p.tool))}\n                from={p.dir === \"call\" ? path.a : path.b}\n                key={p.id}\n                onDone={() => onPulseDone(p)}\n                reduce={reduce}\n                to={p.dir === \"call\" ? path.b : path.a}\n                tone={p.dir}\n              />\n            );\n          })}\n        </svg>\n\n        {tools.map((t) => {\n          const p = pos[t.id];\n          const Icon = t.icon ?? WrenchIcon;\n          const busy = (toolBusy[t.id] ?? 0) > 0;\n          const dim = hover && hover !== t.id && !links.some((l) => l.tool === t.id && l.agent === hover);\n          return (\n            <button\n              aria-label={`${t.name}${busy ? \", running\" : \"\"}`}\n              aria-pressed={pinned === t.id}\n              className={cn(\n                \"absolute flex -translate-x-1/2 -translate-y-1/2 items-center gap-1 whitespace-nowrap rounded-full border bg-background py-0.5 pr-2 pl-1.5 after:absolute after:inset-x-0 after:-inset-y-1.5 font-mono text-[11px] text-muted-foreground outline-none transition-[color,border-color,opacity,box-shadow,scale] duration-300 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97]\",\n                pinned === t.id && \"border-foreground/30 text-foreground\",\n                busy && \"border-info/40 text-foreground ring-3 ring-info/12\",\n                dim && \"border-border/50 text-muted-foreground/40 [&_svg]:opacity-40\",\n              )}\n              key={t.id}\n              onBlur={() => setHover(null)}\n              onFocus={() => setHover(t.id)}\n              onPointerEnter={(e) => e.pointerType === \"mouse\" && setHover(t.id)}\n              onClick={() => setPinned((x) => (x === t.id ? null : t.id))}\n              onPointerLeave={() => setHover(null)}\n              style={{ left: `${(p.x / W) * 100}%`, top: `${(p.y / H) * 100}%` }}\n              type=\"button\"\n            >\n              <Icon className={cn(\"size-3 shrink-0 transition-colors\", busy && \"text-info\")} />\n              {t.name}\n            </button>\n          );\n        })}\n\n        {agents.map((a) => {\n          const p = pos[a.id];\n          const Icon = a.icon ?? BotIcon;\n          const s = status[a.id];\n          const dim = hover && hover !== a.id && !links.some((l) => l.agent === a.id && l.tool === hover);\n          return (\n            <button\n              aria-label={`${a.name}: ${s === \"waiting\" ? \"needs your approval\" : s}`}\n              aria-pressed={pinned === a.id}\n              className={cn(\n                \"group absolute flex -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-1 rounded-full outline-none transition-[scale] duration-150 ease-out active:scale-[0.97]\",\n              )}\n              key={a.id}\n              onBlur={() => setHover(null)}\n              onFocus={() => setHover(a.id)}\n              onPointerEnter={(e) => e.pointerType === \"mouse\" && setHover(a.id)}\n              onClick={() => setPinned((x) => (x === a.id ? null : a.id))}\n              onPointerLeave={() => setHover(null)}\n              style={{ left: `${(p.x / W) * 100}%`, top: `${(p.y / H) * 100}%` }}\n              type=\"button\"\n            >\n              <span className=\"relative flex size-10 items-center justify-center\">\n                <span className=\"absolute inset-0 rounded-full bg-card\" />\n                {s !== \"idle\" && !reduce && !dim && (\n                  <motion.span\n                    animate={{ scale: [1, 1.35, 1], opacity: [0.5, 0, 0.5] }}\n                    className={cn(\"absolute inset-0 rounded-full\", s === \"waiting\" ? \"bg-warning/40\" : \"bg-info/25\")}\n                    transition={{ duration: s === \"waiting\" ? 1.4 : 2.4, repeat: Number.POSITIVE_INFINITY, ease: \"easeInOut\" }}\n                  />\n                )}\n                <span\n                  className={cn(\n                    \"relative flex size-10 items-center justify-center rounded-full border bg-card shadow-xs/5 transition-[color,background-color,border-color,opacity] duration-300 group-focus-visible:ring-2 group-focus-visible:ring-ring\",\n                    dim && \"opacity-40\",\n                    s === \"working\" && \"border-info/50 text-info-foreground\",\n                    s === \"waiting\" && \"border-warning bg-warning text-white\",\n                    s === \"idle\" && \"text-muted-foreground\",\n                  )}\n                >\n                  <AnimatePresence initial={false} mode=\"popLayout\">\n                    <motion.span\n                      animate={{ scale: 1, opacity: 1 }}\n                      exit={{ scale: 0.9, opacity: 0 }}\n                      initial={{ scale: 0.9, opacity: 0 }}\n                      key={s === \"waiting\" ? \"hand\" : \"icon\"}\n                      transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}\n                    >\n                      {s === \"waiting\" ? <HandIcon className=\"size-4\" /> : <Icon className=\"size-4\" />}\n                    </motion.span>\n                  </AnimatePresence>\n                </span>\n              </span>\n              <span\n                className={cn(\n                  \"rounded-full px-1 font-medium text-[11px] leading-tight transition-opacity duration-300\",\n                  dim && \"opacity-40\",\n                  s === \"waiting\" ? \"text-warning-foreground\" : s === \"idle\" ? \"text-muted-foreground\" : \"text-foreground\",\n                )}\n              >\n                {a.name}\n              </span>\n            </button>\n          );\n        })}\n      </div>\n\n      <div className=\"relative h-[68px] border-t bg-muted/40\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {ask && askAgent ? (\n            <motion.div\n              animate={{ opacity: 1, y: 0 }}\n              className=\"absolute inset-0 flex items-center gap-3 px-4\"\n              exit={{ opacity: 0, y: 8 }}\n              initial={{ opacity: 0, y: 8 }}\n              key=\"ask\"\n              role=\"alert\"\n              transition={{ duration: 0.22, ease: [0.23, 1, 0.32, 1] }}\n            >\n              <span className=\"flex size-7 shrink-0 items-center justify-center rounded-full bg-warning/12 text-warning-foreground\">\n                <HandIcon className=\"size-3.5\" />\n              </span>\n              <div className=\"min-w-0 flex-1 text-xs\">\n                <p className=\"font-medium text-foreground text-sm\">{askAgent.name} needs you</p>\n                <p className=\"truncate text-muted-foreground\">\n                  Wants to <span className=\"font-mono text-foreground\">{ask.action}</span>\n                  {askTool ? ` via ${askTool.name}` : \"\"}\n                </p>\n              </div>\n              <div className=\"flex shrink-0 items-center gap-1.5\">\n                <Button aria-label=\"Deny\" className=\"transition-[scale,box-shadow] duration-150 ease-out active:scale-[0.97]\" onClick={() => resolve(false)} size=\"icon-sm\" variant=\"outline\">\n                  <XIcon />\n                </Button>\n                <Button className=\"transition-[scale,box-shadow] duration-150 ease-out active:scale-[0.97]\" onClick={() => resolve(true)} size=\"sm\">\n                  <CheckIcon />\n                  Approve\n                </Button>\n              </div>\n            </motion.div>\n          ) : (\n            <motion.ul\n              animate={{ opacity: 1, y: 0 }}\n              aria-live=\"polite\"\n              className=\"absolute inset-0 flex flex-col gap-0.5 overflow-hidden px-4 pt-2.5 font-mono text-[11px]\"\n              exit={{ opacity: 0, y: -8 }}\n              initial={{ opacity: 0, y: -8 }}\n              key=\"log\"\n              transition={{ duration: 0.22, ease: [0.23, 1, 0.32, 1] }}\n            >\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                {log.length === 0 && <li className=\"text-muted-foreground\">Waiting for the first call…</li>}\n                {log.map((l, i) => (\n                  <motion.li\n                    animate={{ opacity: 1, y: 0 }}\n                    className={cn(\n                      \"flex h-4 items-center gap-1.5 overflow-hidden whitespace-nowrap leading-4\",\n                      i > 0 && \"[&>span]:text-muted-foreground\",\n                    )}\n                    exit={{ opacity: 0, transition: { duration: 0.12 } }}\n                    initial={{ opacity: 0, y: -6 }}\n                    key={l.id}\n                    layout=\"position\"\n                    transition={{ duration: 0.22, ease: [0.23, 1, 0.32, 1] }}\n                  >\n                    <span\n                      className={cn(\n                        \"size-1.5 shrink-0 rounded-full\",\n                        l.tone === \"call\" && \"bg-success\",\n                        l.tone === \"ok\" && \"bg-info\",\n                        l.tone === \"warn\" && \"bg-warning\",\n                        l.tone === \"deny\" && \"bg-destructive\",\n                      )}\n                      style={{ opacity: 1 - i * 0.3 }}\n                    />\n                    <span className=\"text-foreground\">{l.agent}</span>\n                    <span className=\"text-muted-foreground\">→</span>\n                    <span className=\"truncate text-foreground\">{l.tool}</span>\n                    <span className=\"ml-auto shrink-0 text-muted-foreground tabular-nums\">{l.text}</span>\n                  </motion.li>\n                ))}\n              </AnimatePresence>\n            </motion.ul>\n          )}\n        </AnimatePresence>\n      </div>\n    </div>\n    </MotionConfig>\n  );\n}\n"
    }
  ]
}