{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-asistant",
  "title": "AI Asistant",
  "description": "A dotted status light with two layouts, Mesh grid and Halo rings, across three animated states (Waiting, Listening, Thinking), in four dot shapes with adjustable colors.",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "registry/new-york/ai-asistant/ai-asistant.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nfunction cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport type AIAsistantState = \"waiting\" | \"listening\" | \"thinking\";\nexport type AIAsistantTheme = \"mesh\" | \"halo\";\nexport type AIAsistantShape = \"circle\" | \"square\" | \"diamond\" | \"ring\";\n\nexport interface AIAsistantProps extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  size?: number;\n  state?: AIAsistantState;\n  theme?: AIAsistantTheme;\n  shape?: AIAsistantShape;\n  colorA?: string;\n  colorB?: string;\n  dots?: number;\n}\n\nconst STATE_LABEL: Record<AIAsistantState, string> = {\n  waiting: \"Waiting\",\n  listening: \"Listening\",\n  thinking: \"Thinking\",\n};\n\nconst THEME_LABEL: Record<AIAsistantTheme, string> = {\n  mesh: \"Mesh\",\n  halo: \"Halo\",\n};\n\nconst TAU = Math.PI * 2;\nconst SEO_TEXT = \"AI Asistant is a dotted status light for AI chat products. Mesh and Halo themes, with Waiting, Listening and Thinking states.\";\n\nconst SR_ONLY_STYLE: React.CSSProperties = {\n  position: \"absolute\",\n  width: 1,\n  height: 1,\n  overflow: \"hidden\",\n  clip: \"rect(0 0 0 0)\",\n};\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  let h = hex.replace(\"#\", \"\").trim();\n  if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n  const n = parseInt(h, 16);\n  if (h.length !== 6 || Number.isNaN(n)) return [153, 94, 36];\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction mixHex(a: string, b: string, t: number) {\n  const A = hexToRgb(a);\n  const B = hexToRgb(b);\n  const k = Math.min(1, Math.max(0, t));\n  return `rgb(${Math.round(A[0] + (B[0] - A[0]) * k)},${Math.round(A[1] + (B[1] - A[1]) * k)},${Math.round(A[2] + (B[2] - A[2]) * k)})`;\n}\n\nfunction hash(ix: number, iy: number) {\n  const n = Math.sin(ix * 127.1 + iy * 311.7) * 43758.5453;\n  return n - Math.floor(n);\n}\n\nfunction roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, rad: number) {\n  const r = Math.min(rad, w / 2, h / 2);\n  ctx.beginPath();\n  ctx.moveTo(x + r, y);\n  ctx.arcTo(x + w, y, x + w, y + h, r);\n  ctx.arcTo(x + w, y + h, x, y + h, r);\n  ctx.arcTo(x, y + h, x, y, r);\n  ctx.arcTo(x, y, x + w, y, r);\n  ctx.closePath();\n}\n\nfunction drawShape(ctx: CanvasRenderingContext2D, x: number, y: number, r: number, shape: AIAsistantShape, color: string) {\n  ctx.save();\n  ctx.translate(x, y);\n  ctx.fillStyle = color;\n  ctx.strokeStyle = color;\n  ctx.lineCap = \"round\";\n  ctx.lineJoin = \"round\";\n\n  if (shape === \"circle\") {\n    ctx.beginPath();\n    ctx.arc(0, 0, r, 0, TAU);\n    ctx.fill();\n  } else if (shape === \"square\") {\n    const s = r * 1.55;\n    roundRect(ctx, -s / 2, -s / 2, s, s, s * 0.34);\n    ctx.fill();\n  } else if (shape === \"diamond\") {\n    const q = r * 1.15;\n    ctx.beginPath();\n    ctx.moveTo(0, -q * 1.15);\n    ctx.quadraticCurveTo(q * 0.22, -q * 0.22, q, 0);\n    ctx.quadraticCurveTo(q * 0.22, q * 0.22, 0, q * 1.15);\n    ctx.quadraticCurveTo(-q * 0.22, q * 0.22, -q, 0);\n    ctx.quadraticCurveTo(-q * 0.22, -q * 0.22, 0, -q * 1.15);\n    ctx.closePath();\n    ctx.fill();\n  } else {\n    ctx.beginPath();\n    ctx.arc(0, 0, r * 0.92, 0, TAU);\n    ctx.lineWidth = Math.max(1.4, r * 0.38);\n    ctx.stroke();\n  }\n  ctx.restore();\n}\n\ntype Dot = { x: number; y: number; s: number; tone: number };\n\nfunction layout(theme: AIAsistantTheme, state: AIAsistantState, grid: number, t: number): Dot[] {\n  const dots: Dot[] = [];\n  const half = (grid - 1) / 2;\n  const listen = state === \"listening\" ? 1 : 0;\n  const think = state === \"thinking\" ? 1 : 0;\n  const wait = state === \"waiting\" ? 1 : 0;\n\n  if (theme === \"halo\") {\n    const rings = Math.max(3, Math.round(grid / 3));\n    const pulse = 1 + wait * 0.045 * Math.sin(t * 1.1);\n    for (let ring = 0; ring < rings; ring++) {\n      const radius = (0.18 + (ring / Math.max(1, rings - 1)) * 0.78) * pulse;\n      const count = 6 + ring * 4;\n      const dir = ring % 2 === 0 ? 1 : -1;\n      const spin = wait ? 0 : t * (0.18 + think * 0.28 + listen * 0.12) * dir;\n      const listenPulse = listen * 0.05 * Math.sin(t * 2.4 - ring * 0.8);\n      for (let i = 0; i < count; i++) {\n        const a = (i / count) * TAU + spin;\n        const sweep = think ? 0.5 + 0.5 * Math.sin(a * 2 + t * 1.6) : ring / Math.max(1, rings - 1);\n        dots.push({\n          x: Math.cos(a) * radius * (1 + listenPulse),\n          y: Math.sin(a) * radius * (1 + listenPulse),\n          s: 0.72 - ring * 0.06 + listen * 0.08,\n          tone: sweep,\n        });\n      }\n    }\n    return dots;\n  }\n\n  for (let iy = 0; iy < grid; iy++) {\n    for (let ix = 0; ix < grid; ix++) {\n      const stagger = iy % 2 === 1 ? 0.5 : 0;\n      const nx = (ix + stagger - half) / half;\n      const ny = (iy - half) / half;\n      const d = Math.hypot(nx, ny);\n      if (d > 1.12) continue;\n\n      const breathe = 1 + wait * 0.04 * Math.sin(t * 1.1 + d * 2);\n      const ripple = listen * 0.07 * Math.sin(d * 7 - t * 2.8);\n      const hop = think * 0.06 * Math.sin(hash(ix, iy) * TAU + t * 2.4);\n\n      let tone = 0.5 + nx * 0.42;\n      if (think) {\n        const band = 0.5 + 0.5 * Math.sin(nx * 3.2 + ny * 1.4 + t * 1.8);\n        tone = 0.08 + 0.84 * band;\n      } else if (listen) {\n        tone = 0.35 + 0.45 * (0.5 + 0.5 * Math.sin(d * 5 - t * 2.2));\n      }\n\n      dots.push({\n        x: nx * (breathe + ripple),\n        y: ny * (breathe + ripple),\n        s: 0.5 + 0.5 * Math.exp(-d * d * 0.9) + hop,\n        tone,\n      });\n    }\n  }\n  return dots;\n}\n\n/**\n * AIAsistant — a dotted status light with two layouts (Mesh grid, Halo\n * rings) across three animated states (Waiting, Listening, Thinking), in\n * four dot shapes. Pauses its render loop while off-screen and respects\n * prefers-reduced-motion.\n */\nexport function AIAsistant({\n  className,\n  style,\n  size = 0,\n  state = \"waiting\",\n  theme = \"mesh\",\n  shape = \"square\",\n  colorA = \"#995E24\",\n  colorB = \"#221307\",\n  dots = 13,\n  ...props\n}: AIAsistantProps) {\n  const wrapRef = React.useRef<HTMLDivElement>(null);\n  const stageRef = React.useRef<HTMLDivElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const isVisibleRef = React.useRef(true);\n  const rafRef = React.useRef(0);\n\n  const useFixed = typeof size === \"number\" && size > 0;\n\n  const liveRef = React.useRef({ state, theme, shape, a: colorA, b: colorB, dots });\n  React.useEffect(() => {\n    liveRef.current = { state, theme, shape, a: colorA, b: colorB, dots };\n  }, [state, theme, shape, colorA, colorB, dots]);\n\n  React.useEffect(() => {\n    const el = wrapRef.current;\n    if (!el) return;\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        isVisibleRef.current = entry.isIntersecting;\n      },\n      { threshold: 0 }\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, []);\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    const stage = stageRef.current;\n    if (!canvas || !stage) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    let running = true;\n    let last = performance.now();\n    let time = 0;\n    let side = 240;\n\n    const reduce = typeof window.matchMedia === \"function\" && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    const fit = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const w = Math.max(2, Math.round(stage.clientWidth * dpr));\n      const h = Math.max(2, Math.round(stage.clientHeight * dpr));\n      const buffer = Math.min(w, h);\n      if (canvas.width !== buffer || canvas.height !== buffer) {\n        canvas.width = buffer;\n        canvas.height = buffer;\n      }\n      side = buffer / dpr;\n      ctx.setTransform(buffer / side, 0, 0, buffer / side, 0, 0);\n    };\n\n    const draw = (t: number) => {\n      const L = liveRef.current;\n      const grid = Math.max(6, Math.round(L.dots));\n      const list = layout(L.theme, L.state, grid, t);\n      const radius = side * 0.42;\n      const center = side / 2;\n      const base = (side * 0.72) / grid;\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n\n      ctx.clearRect(0, 0, side, side);\n      for (const p of list) {\n        const r = base * 0.4 * p.s;\n        if (r * dpr < 0.45) continue;\n        drawShape(ctx, center + p.x * radius, center + p.y * radius, r, L.shape, mixHex(L.a, L.b, Math.min(1, Math.max(0, p.tone))));\n      }\n    };\n\n    fit();\n    const ro = new ResizeObserver(() => fit());\n    ro.observe(stage);\n\n    if (reduce) {\n      draw(0);\n      return () => ro.disconnect();\n    }\n\n    const frame = (now: number) => {\n      if (!running) return;\n      const dt = Math.min((now - last) / 1000, 0.05);\n      last = now;\n      time += dt;\n      if (isVisibleRef.current) draw(time);\n      rafRef.current = requestAnimationFrame(frame);\n    };\n\n    rafRef.current = requestAnimationFrame(frame);\n\n    return () => {\n      running = false;\n      cancelAnimationFrame(rafRef.current);\n      ro.disconnect();\n    };\n  }, []);\n\n  return (\n    <div\n      ref={wrapRef}\n      role=\"img\"\n      aria-label={`AI Asistant, ${THEME_LABEL[theme]}, ${STATE_LABEL[state]}`}\n      className={cn(\"relative\", className)}\n      style={{\n        width: \"100%\",\n        height: \"100%\",\n        overflow: \"visible\",\n        ...style,\n        ...(useFixed ? { width: size, height: size } : null),\n      }}\n      {...props}\n    >\n      <span style={SR_ONLY_STYLE}>{SEO_TEXT}</span>\n\n      <div ref={stageRef} style={{ position: \"absolute\", inset: 0, overflow: \"hidden\", background: \"transparent\" }}>\n        <canvas ref={canvasRef} aria-hidden=\"true\" style={{ width: \"100%\", height: \"100%\", display: \"block\" }} />\n      </div>\n    </div>\n  );\n}\n\nexport default AIAsistant;\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}