{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-avatar",
  "type": "registry:ui",
  "title": "Agent avatar",
  "description": "An agent's face: coloured disc, geometric head with eyes, translucent shoulders; eight shapes, eight colours, a default derived from the agent id, static or animated (idle, thinking, greeting). Its colours and motion live in the Wibo stylesheet (@wibo/styles).",
  "registryDependencies": [
    "@wibo/utils"
  ],
  "files": [
    {
      "path": "src/components/agent-avatar.tsx",
      "type": "registry:ui",
      "content": "import { useId, type ComponentProps, type CSSProperties } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/*\n * An agent's face: a coloured disc, a white geometric head with two round\n * eyes in the disc's colour, and translucent shoulders rising from the\n * bottom edge, clipped by the disc. Flat, no outlines, no gradients.\n *\n * Every agent has one before anyone picks it: the shape and the colour are\n * derived from a stable hash of the agent id (`agentAvatarDefaults`), and a\n * tenant's admin may choose others (stored per tenant and agent by the app).\n * Unknown or missing ids fall back to that default, so a stored value from a\n * newer palette never breaks an older screen.\n *\n * Motion (CSS only, styles in `styles.css`, cut under reduced motion):\n *   static    lists, tables, dense UI\n *   idle      an occasional blink and a slow breath of the shoulders, timed\n *             per instance so a page of avatars never blinks in sync\n *   thinking  while the agent works: the eyes glance around, the head\n *             ponders, the shoulders glow softly\n *   greeting  a small hop on mount, then idle\n * Idle and greeting avatars also hop when hovered.\n *\n * Below 28 px the head and the eyes are drawn larger, so the eyes stay\n * visible at 16 and 20 px. Pure SVG, no state: renders in server components.\n */\n\n/** Shape ids, stable: stored per tenant and agent. Append only, the default hash depends on the order. */\nconst AGENT_AVATAR_SHAPES = [\"circle\", \"square\", \"wide\", \"tall\", \"triangle\", \"pentagon\", \"hexagon\", \"diamond\"] as const;\n/** Colour ids, stable: each is the token `--avatar-<id>`. Append only, the default hash depends on the order. */\nconst AGENT_AVATAR_COLORS = [\"red\", \"orange\", \"amber\", \"green\", \"teal\", \"blue\", \"indigo\", \"pink\"] as const;\n\ntype AgentAvatarShape = (typeof AGENT_AVATAR_SHAPES)[number];\ntype AgentAvatarColor = (typeof AGENT_AVATAR_COLORS)[number];\ntype AgentAvatarMotion = \"static\" | \"idle\" | \"thinking\" | \"greeting\";\n\ninterface AgentAvatarLook {\n  shape: AgentAvatarShape;\n  color: AgentAvatarColor;\n}\n\nfunction isAgentAvatarShape(value: unknown): value is AgentAvatarShape {\n  return typeof value === \"string\" && (AGENT_AVATAR_SHAPES as readonly string[]).includes(value);\n}\n\nfunction isAgentAvatarColor(value: unknown): value is AgentAvatarColor {\n  return typeof value === \"string\" && (AGENT_AVATAR_COLORS as readonly string[]).includes(value);\n}\n\n/** FNV-1a over the UTF-16 code units, then murmur3's finaliser so every bit avalanches. Unsigned 32-bit. */\nfunction hash32(text: string): number {\n  let h = 0x811c9dc5;\n  for (let i = 0; i < text.length; i++) {\n    h ^= text.charCodeAt(i);\n    h = Math.imul(h, 0x01000193);\n  }\n  h ^= h >>> 16;\n  h = Math.imul(h, 0x85ebca6b);\n  h ^= h >>> 13;\n  h = Math.imul(h, 0xc2b2ae35);\n  h ^= h >>> 16;\n  return h >>> 0;\n}\n\n/** The look an agent has until someone chooses one: the same for an id on every screen and every tenant. */\nfunction agentAvatarDefaults(agentId: string): AgentAvatarLook {\n  const h = hash32(agentId);\n  return {\n    color: AGENT_AVATAR_COLORS[h % AGENT_AVATAR_COLORS.length] as AgentAvatarColor,\n    shape: AGENT_AVATAR_SHAPES[Math.floor(h / AGENT_AVATAR_COLORS.length) % AGENT_AVATAR_SHAPES.length] as AgentAvatarShape,\n  };\n}\n\n/** The look to draw: the stored shape and colour when they are known ids, the default for the agent otherwise. */\nfunction resolveAgentAvatar(agentId: string, shape?: string | null, color?: string | null): AgentAvatarLook {\n  const fallback = agentAvatarDefaults(agentId);\n  return {\n    shape: isAgentAvatarShape(shape) ? shape : fallback.shape,\n    color: isAgentAvatarColor(color) ? color : fallback.color,\n  };\n}\n\n/* The disc's colour is the svg's `color`; the disc and the eyes paint `currentColor`. */\nconst COLOR_CLASS: Record<AgentAvatarColor, string> = {\n  red: \"text-avatar-red\",\n  orange: \"text-avatar-orange\",\n  amber: \"text-avatar-amber\",\n  green: \"text-avatar-green\",\n  teal: \"text-avatar-teal\",\n  blue: \"text-avatar-blue\",\n  indigo: \"text-avatar-indigo\",\n  pink: \"text-avatar-pink\",\n};\n\n/* ---------------------------------------------------------------------- */\n/* Geometry, in a 48 x 48 view box. Heads are drawn around their centre.   */\n/* ---------------------------------------------------------------------- */\n\ntype Point = readonly [number, number];\n\nconst r2 = (n: number) => Math.round(n * 100) / 100;\n\n/** A closed polygon, clockwise on screen, every corner rounded by a tangent arc of `radius`. */\nfunction roundedPolygon(points: readonly Point[], radius: number): string {\n  let d = \"\";\n  points.forEach((corner, i) => {\n    const prev = points[(i + points.length - 1) % points.length] as Point;\n    const next = points[(i + 1) % points.length] as Point;\n    const toPrev = [prev[0] - corner[0], prev[1] - corner[1]] as const;\n    const toNext = [next[0] - corner[0], next[1] - corner[1]] as const;\n    const lenPrev = Math.hypot(toPrev[0], toPrev[1]);\n    const lenNext = Math.hypot(toNext[0], toNext[1]);\n    const angle = Math.acos((toPrev[0] * toNext[0] + toPrev[1] * toNext[1]) / (lenPrev * lenNext));\n    const cut = radius / Math.tan(angle / 2);\n    const from = [corner[0] + (toPrev[0] / lenPrev) * cut, corner[1] + (toPrev[1] / lenPrev) * cut];\n    const to = [corner[0] + (toNext[0] / lenNext) * cut, corner[1] + (toNext[1] / lenNext) * cut];\n    d += `${i === 0 ? \"M\" : \"L\"}${r2(from[0]!)} ${r2(from[1]!)}A${radius} ${radius} 0 0 1 ${r2(to[0]!)} ${r2(to[1]!)}`;\n  });\n  return `${d}Z`;\n}\n\n/** A regular polygon around the origin, the first corner at `start` degrees (clockwise on screen). */\nfunction regular(sides: number, radius: number, start: number): Point[] {\n  return Array.from({ length: sides }, (_, i) => {\n    const a = ((start + (360 / sides) * i) * Math.PI) / 180;\n    return [radius * Math.cos(a), radius * Math.sin(a)] as const;\n  });\n}\n\nfunction roundedRect(width: number, height: number, radius: number): string {\n  const x = -width / 2;\n  const y = -height / 2;\n  return `M${r2(x + radius)} ${r2(y)}H${r2(x + width - radius)}A${radius} ${radius} 0 0 1 ${r2(x + width)} ${r2(y + radius)}V${r2(y + height - radius)}A${radius} ${radius} 0 0 1 ${r2(x + width - radius)} ${r2(y + height)}H${r2(x + radius)}A${radius} ${radius} 0 0 1 ${r2(x)} ${r2(y + height - radius)}V${r2(y + radius)}A${radius} ${radius} 0 0 1 ${r2(x + radius)} ${r2(y)}Z`;\n}\n\nfunction circle(radius: number): string {\n  return `M${-radius} 0A${radius} ${radius} 0 1 1 ${radius} 0A${radius} ${radius} 0 1 1 ${-radius} 0Z`;\n}\n\ninterface Head {\n  /** The head's outline around its centre. */\n  d: string;\n  /** Vertical offset of the head's centre from the common head line. */\n  dy: number;\n  /** Where the eyes sit: their line below the centre and their distance from the middle. */\n  eyeY: number;\n  eyeX: number;\n}\n\n/*\n * Sizes are tuned for equal visual weight (areas within a few percent):\n * pointed shapes run larger than the circle, the wide screen and the tall\n * capsule trade width for height. Eyes sit a little below the centre, as\n * in the drawings; the triangle keeps them low and close, where it is wide.\n */\nconst HEADS: Record<AgentAvatarShape, Head> = {\n  circle: { d: circle(10.2), dy: 0, eyeY: 0.6, eyeX: 3.1 },\n  square: { d: roundedRect(18.6, 18.6, 5.2), dy: 0, eyeY: 1.2, eyeX: 3.1 },\n  wide: { d: roundedRect(24.6, 16, 5.4), dy: 0.4, eyeY: 0.4, eyeX: 3.6 },\n  tall: { d: roundedRect(16, 22, 6.4), dy: -0.2, eyeY: -0.2, eyeX: 2.9 },\n  triangle: { d: roundedPolygon([[0, -14], [13.6, 8.2], [-13.6, 8.2]], 3), dy: -0.4, eyeY: 3.4, eyeX: 2.6 },\n  pentagon: { d: roundedPolygon(regular(5, 11.8, -90), 3), dy: -0.4, eyeY: 1.4, eyeX: 3.1 },\n  hexagon: { d: roundedPolygon(regular(6, 11.4, -120), 3), dy: 0, eyeY: 0.8, eyeX: 3.3 },\n  diamond: { d: roundedPolygon(regular(4, 12.4, -90), 3.6), dy: -1, eyeY: 0.6, eyeX: 2.8 },\n};\n\n/** Two levels of detail: `compact` below 28 px draws the eyes larger and further apart, so they stay readable. */\nconst DETAIL = {\n  regular: { headY: 19.6, headScale: 1, eyeR: 1.75, eyeSpread: 1, shouldersTop: 34 },\n  compact: { headY: 19.2, headScale: 1.05, eyeR: 2.4, eyeSpread: 1.3, shouldersTop: 35 },\n} as const;\n\n/** The shoulders: a wide dome rising from the bottom edge (the disc clips it). */\nfunction shoulders(top: number): string {\n  const rx = 16.5;\n  const ry = 13;\n  const cy = top + ry;\n  return `M${r2(24 - rx)} ${r2(cy)}A${rx} ${ry} 0 0 1 ${r2(24 + rx)} ${r2(cy)}V60H${r2(24 - rx)}Z`;\n}\n\n/* Per-instance timing, so a page of avatars breathes and blinks out of step. */\nfunction timing(seed: string): CSSProperties {\n  const h = hash32(seed);\n  const blink = 4.6 + ((h & 0xff) / 255) * 3.4;\n  const breath = 4.2 + (((h >>> 8) & 0xff) / 255) * 1.6;\n  return {\n    \"--agent-avatar-blink\": `${blink.toFixed(2)}s`,\n    \"--agent-avatar-blink-delay\": `${(-(((h >>> 16) & 0xff) / 255) * blink).toFixed(2)}s`,\n    \"--agent-avatar-breath\": `${breath.toFixed(2)}s`,\n    \"--agent-avatar-breath-delay\": `${(-(((h >>> 24) & 0xff) / 255) * breath).toFixed(2)}s`,\n  } as CSSProperties;\n}\n\ninterface AgentAvatarProps extends Omit<ComponentProps<\"svg\">, \"color\" | \"children\"> {\n  /** The agent's id: seeds the default look and the motion's timing. */\n  agentId: string;\n  /** A shape id (`AGENT_AVATAR_SHAPES`); unknown or missing takes the default. */\n  shape?: string | null;\n  /** A colour id (`AGENT_AVATAR_COLORS`); unknown or missing takes the default. */\n  color?: string | null;\n  /** Rendered size in pixels: 16 to 96 and up. Default 32. */\n  size?: number;\n  motion?: AgentAvatarMotion;\n  /**\n   * The agent's name: the image's accessible name. Leave it out when the\n   * name is written next to the avatar; the avatar is then decorative.\n   */\n  label?: string;\n}\n\n/** An agent's face: disc, head, eyes and shoulders, static or animated. */\nfunction AgentAvatar({ agentId, shape, color, size = 32, motion = \"static\", label, className, style, ...props }: AgentAvatarProps) {\n  const id = useId();\n  const look = resolveAgentAvatar(agentId, shape, color);\n  const head = HEADS[look.shape];\n  const detail = size < 28 ? DETAIL.compact : DETAIL.regular;\n  const clip = `${id}-disc`;\n  const eyeX = head.eyeX * detail.eyeSpread;\n  const blinkTwice = hash32(`${agentId}:${id}`) % 3 === 0;\n\n  return (\n    <svg\n      data-slot=\"agent-avatar\"\n      data-shape={look.shape}\n      data-color={look.color}\n      data-motion={motion}\n      data-blink={blinkTwice ? \"twice\" : \"once\"}\n      viewBox=\"0 0 48 48\"\n      width={size}\n      height={size}\n      role={label ? \"img\" : undefined}\n      aria-label={label}\n      aria-hidden={label ? undefined : true}\n      focusable=\"false\"\n      className={cn(\"agent-avatar shrink-0 select-none\", COLOR_CLASS[look.color], className)}\n      style={motion === \"static\" ? style : { ...timing(`${agentId}:${id}`), ...style }}\n      {...props}\n    >\n      <defs>\n        <clipPath id={clip}>\n          <circle cx=\"24\" cy=\"24\" r=\"24\" />\n        </clipPath>\n      </defs>\n      <circle cx=\"24\" cy=\"24\" r=\"24\" className=\"fill-current\" />\n      <g clipPath={`url(#${clip})`}>\n        <g className=\"agent-avatar-body fill-avatar-face\" opacity={0.4}>\n          <path d={shoulders(detail.shouldersTop)} />\n        </g>\n      </g>\n      <g transform={`translate(24 ${r2(detail.headY + head.dy * detail.headScale)}) scale(${detail.headScale})`}>\n        <g className=\"agent-avatar-head\">\n          <path d={head.d} className=\"fill-avatar-face\" />\n          <g className=\"agent-avatar-look\">\n            <g className=\"agent-avatar-eyes fill-current\">\n              <circle cx={-eyeX} cy={head.eyeY} r={detail.eyeR} />\n              <circle cx={eyeX} cy={head.eyeY} r={detail.eyeR} />\n            </g>\n          </g>\n        </g>\n      </g>\n    </svg>\n  );\n}\n\nexport {\n  AGENT_AVATAR_COLORS,\n  AGENT_AVATAR_SHAPES,\n  AgentAvatar,\n  agentAvatarDefaults,\n  isAgentAvatarColor,\n  isAgentAvatarShape,\n  resolveAgentAvatar,\n};\nexport type { AgentAvatarColor, AgentAvatarLook, AgentAvatarMotion, AgentAvatarProps, AgentAvatarShape };\n"
    }
  ]
}
