{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "typing-indicator",
  "type": "registry:component",
  "title": "Typing Indicator",
  "description": "A \"typing…\" bubble with three elastically bouncing dots that, in a loop, turns into the reply with a little spring.",
  "author": "Marcelo Treviño Velázquez <https://furin.dev>",
  "dependencies": [],
  "categories": [
    "Loaders"
  ],
  "docs": "Installed in components/furin/typing-indicator/. Docs, live controls and props table: https://furin.dev/components/typing-indicator/",
  "files": [
    {
      "path": "registry/typing-indicator/TypingIndicator.tsx",
      "content": "\"use client\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { CSSProperties } from \"react\";\nimport \"./TypingIndicator.css\";\n\nexport interface TypingIndicatorProps {\n  /** Muestra un mensaje anterior para dar contexto a la burbuja. */\n  context?: string;\n  /** Respuesta que aparece cuando termina de \"escribir\" (vacío para que solo escriba). */\n  reply?: string;\n  /** Segundos que dura el \"escribiendo…\" antes de mostrar la respuesta. */\n  typingTime?: number;\n  /** Segundos que se queda la respuesta antes de volver a empezar. */\n  replyTime?: number;\n  /** Estilo de la burbuja: light, dark o accent. */\n  variant?: \"light\" | \"dark\" | \"accent\";\n  /** Escala de toda la pieza (1 es el tamaño normal). */\n  scale?: number;\n  /** Texto oculto que leen los lectores de pantalla. */\n  label?: string;\n  /** Clase extra para el contenedor. */\n  className?: string;\n  /** Estilos extra para el contenedor. */\n  style?: CSSProperties;\n}\n\n/** Burbuja de \"escribiendo…\": tres puntos con rebote elástico (se estiran, caen y se aplastan en cadena), cola de globo y, en ciclo, la respuesta que reemplaza a la burbuja con un pequeño resorte. */\nexport function TypingIndicator({\n  context = \"Did you see this week's report yet?\",\n  reply = \"Yes, I'll send it over in a moment.\",\n  typingTime = 3.2,\n  replyTime = 3,\n  variant = \"dark\",\n  scale = 1,\n  label = \"Typing\",\n  className = \"\",\n  style,\n}: TypingIndicatorProps) {\n  const [fase, setFase] = useState<\"escribe\" | \"responde\">(\"escribe\");\n  const [ciclo, setCiclo] = useState(0);\n  const raiz = useRef<HTMLDivElement>(null);\n  const [pausa, setPausa] = useState(false);\n\n  useEffect(() => {\n    const el = raiz.current;\n    if (!el) return;\n    const quieto = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    let visible = true;\n    let timer = 0;\n    const programar = (f: \"escribe\" | \"responde\") => {\n      window.clearTimeout(timer);\n      if (quieto || !reply) return;\n      const espera = (f === \"escribe\" ? typingTime : replyTime) * 1000;\n      timer = window.setTimeout(() => {\n        if (!visible) { programar(f); return; }\n        if (f === \"escribe\") { setFase(\"responde\"); programar(\"responde\"); }\n        else { setFase(\"escribe\"); setCiclo((c) => c + 1); programar(\"escribe\"); }\n      }, espera);\n    };\n    programar(\"escribe\");\n    const io = new IntersectionObserver(([e]) => { visible = e.isIntersecting; setPausa(!e.isIntersecting); });\n    io.observe(el);\n    return () => { io.disconnect(); window.clearTimeout(timer); };\n  }, [reply, typingTime, replyTime]);\n\n  const vars = { \"--furin-ti-scale\": scale } as CSSProperties;\n  return (\n    <div ref={raiz} className={`furin-typing-indicator is-${variant} ${className}`} style={{ ...vars, ...style }} role=\"status\" data-pausa={pausa ? \"1\" : \"0\"}>\n      {context ? <div className=\"furin-typing-indicator__ctx\" aria-hidden=\"true\">{context}</div> : null}\n      <div className=\"furin-typing-indicator__slot\" aria-hidden=\"true\">\n        {fase === \"escribe\" ? (\n          <div key={`t${ciclo}`} className=\"furin-typing-indicator__bubble furin-typing-indicator__typing\">\n            <i className=\"furin-typing-indicator__dot\" style={{ \"--furin-ti-i\": 0 } as CSSProperties} />\n            <i className=\"furin-typing-indicator__dot\" style={{ \"--furin-ti-i\": 1 } as CSSProperties} />\n            <i className=\"furin-typing-indicator__dot\" style={{ \"--furin-ti-i\": 2 } as CSSProperties} />\n          </div>\n        ) : (\n          <div key={`r${ciclo}`} className=\"furin-typing-indicator__bubble furin-typing-indicator__reply\">{reply}</div>\n        )}\n      </div>\n      <span className=\"furin-typing-indicator__sr\">{fase === \"escribe\" ? `${label}…` : reply}</span>\n    </div>\n  );\n}\n\nexport default TypingIndicator;\n",
      "type": "registry:component",
      "target": "components/furin/typing-indicator/TypingIndicator.tsx"
    },
    {
      "path": "registry/typing-indicator/TypingIndicator.css",
      "content": ".furin-typing-indicator {\n  --furin-ti-scale: 1;\n  --furin-ti-bg: #2a2b31;\n  --furin-ti-fg: #f3f5f8;\n  --furin-ti-dot: #9a9ca6;\n  --furin-ti-ctx: linear-gradient(180deg, #4a8cff, #2c6bf2);\n  display: grid;\n  gap: 10px;\n  justify-items: start;\n  width: min(340px, 92%);\n  font-family: \"Geist\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  font-size: calc(15px * var(--furin-ti-scale));\n  color: var(--furin-ti-fg);\n}\n.furin-typing-indicator.is-light { --furin-ti-bg: #e9e9ee; --furin-ti-fg: #15161a; --furin-ti-dot: #8b8d97; }\n.furin-typing-indicator.is-accent { --furin-ti-bg: linear-gradient(180deg, #4a8cff, #2c6bf2); --furin-ti-fg: #fff; --furin-ti-dot: rgba(255, 255, 255, 0.9); --furin-ti-ctx: #2a2b31; }\n\n.furin-typing-indicator__ctx {\n  justify-self: end;\n  max-width: 82%;\n  padding: 0.55em 0.95em;\n  border-radius: 1.2em 1.2em 0.35em 1.2em;\n  background: var(--furin-ti-ctx);\n  color: #fff;\n  line-height: 1.3;\n  box-shadow: 0 6px 18px -8px rgba(0, 0, 0, 0.6);\n}\n.is-accent .furin-typing-indicator__ctx { color: #f3f5f8; }\n\n.furin-typing-indicator__slot { position: relative; min-height: 2.6em; }\n.furin-typing-indicator__bubble {\n  position: relative;\n  display: inline-flex;\n  align-items: center;\n  background: var(--furin-ti-bg);\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 6px 18px -8px rgba(0, 0, 0, 0.6);\n  transform-origin: 0% 100%;\n  animation: furin-typing-indicator-pop 0.65s linear(0, 0.35 8%, 0.82 17%, 1.08 26%, 1.14 33%, 1.06 42%, 0.98 53%, 1.01 65%, 1 80%, 1) both;\n}\n.furin-typing-indicator__typing {\n  gap: 0.34em;\n  padding: 0.92em 1.05em;\n  border-radius: 1.4em;\n}\n/* Cola de globo: dos círculos que se achican hacia abajo a la izquierda. */\n.furin-typing-indicator__typing::before,\n.furin-typing-indicator__typing::after {\n  content: \"\";\n  position: absolute;\n  border-radius: 50%;\n  background: var(--furin-ti-bg);\n}\n.furin-typing-indicator__typing::before { width: 0.62em; height: 0.62em; left: -0.18em; bottom: -0.1em; }\n.furin-typing-indicator__typing::after { width: 0.32em; height: 0.32em; left: -0.46em; bottom: -0.34em; }\n\n.furin-typing-indicator__dot {\n  display: block;\n  width: 0.56em;\n  height: 0.56em;\n  border-radius: 50%;\n  background: var(--furin-ti-dot);\n  animation: furin-typing-indicator-dot 1.4s linear infinite;\n  animation-delay: calc(var(--furin-ti-i) * 0.17s);\n}\n@keyframes furin-typing-indicator-dot {\n  0%, 62%, 100% { transform: translateY(0) scale(1, 1); opacity: 0.5; animation-timing-function: ease-in; }\n  8% { transform: translateY(0.05em) scale(1.12, 0.86); opacity: 0.55; animation-timing-function: ease-out; }\n  22% { transform: translateY(-0.52em) scale(0.9, 1.14); opacity: 1; animation-timing-function: ease-in; }\n  38% { transform: translateY(0.1em) scale(1.16, 0.84); opacity: 0.85; animation-timing-function: ease-out; }\n  50% { transform: translateY(-0.14em) scale(0.97, 1.04); opacity: 0.65; animation-timing-function: ease-in-out; }\n}\n.furin-typing-indicator[data-pausa=\"1\"] .furin-typing-indicator__dot { animation-play-state: paused; }\n\n.furin-typing-indicator__reply {\n  max-width: 88%;\n  padding: 0.62em 1em;\n  border-radius: 1.25em 1.25em 1.25em 0.35em;\n  line-height: 1.3;\n}\n\n@keyframes furin-typing-indicator-pop {\n  from { opacity: 0; transform: scale(0.35); }\n  25% { opacity: 1; }\n  to { opacity: 1; transform: scale(1); }\n}\n\n.furin-typing-indicator__sr {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  margin: -1px;\n  padding: 0;\n  overflow: hidden;\n  clip-path: inset(50%);\n  white-space: nowrap;\n  border: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .furin-typing-indicator__bubble { animation: none; }\n  .furin-typing-indicator__dot { animation: none; opacity: 0.8; }\n  .furin-typing-indicator__dot:nth-child(2) { transform: translateY(-0.18em); opacity: 1; }\n}\n",
      "type": "registry:file",
      "target": "components/furin/typing-indicator/TypingIndicator.css"
    }
  ]
}
