// src/utils/stream-animation.ts
var BUILTIN_PLUGINS = [
  {
    name: "typewriter",
    containerClass: "persona-stream-typewriter",
    wrap: "char",
    useCaret: true
  },
  {
    name: "pop-bubble",
    bubbleClass: "persona-stream-pop",
    wrap: "none"
  },
  {
    name: "letter-rise",
    containerClass: "persona-stream-letter-rise",
    wrap: "char"
  },
  {
    name: "word-fade",
    containerClass: "persona-stream-word-fade",
    wrap: "word"
  }
];
var globalRegistry = /* @__PURE__ */ new Map();
for (const plugin of BUILTIN_PLUGINS) globalRegistry.set(plugin.name, plugin);
var registerStreamAnimationPlugin = (plugin) => {
  globalRegistry.set(plugin.name, plugin);
};

// src/animations/glyph-cycle.ts
var STYLES = `
[data-persona-root] .persona-stream-glyph-cycle .persona-stream-char {
  animation: persona-stream-glyph-cycle-fade
    calc(var(--persona-stream-step, 120ms) * 1.5) ease-out both;
}
[data-persona-root] .persona-stream-glyph-cycle .persona-stream-char[data-glyph-cycle-final] {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
@keyframes persona-stream-glyph-cycle-fade {
  from { opacity: 0.35; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  [data-persona-root] .persona-stream-glyph-cycle .persona-stream-char {
    animation: none !important;
    opacity: 1 !important;
  }
}
`.trim();
var GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&@";
var TICK_COUNT = 10;
var BASE_TICK_MS = 120;
var DEFAULT_STEP_MS = 120;
var CROSS_FLICKER_PROBABILITY = 0.4;
var BUFFER_THRESHOLD = 50;
var getStepMs = (container) => {
  var _a;
  if (!container) return DEFAULT_STEP_MS;
  const raw = (_a = container.style.getPropertyValue("--persona-stream-step")) == null ? void 0 : _a.trim();
  const match = raw.match(/([\d.]+)\s*ms/);
  return match ? parseFloat(match[1]) : DEFAULT_STEP_MS;
};
var getTickMs = (span) => {
  const container = span.closest(".persona-stream-glyph-cycle");
  const step = getStepMs(container);
  return BASE_TICK_MS * step / DEFAULT_STEP_MS;
};
var randomGlyph = (avoid) => {
  let ch = GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
  if (avoid && ch === avoid) {
    ch = GLYPHS[(GLYPHS.indexOf(ch) + 1) % GLYPHS.length];
  }
  return ch;
};
var flickerLaterSiblings = (span) => {
  const container = span.closest(".persona-stream-glyph-cycle");
  if (!container) return;
  const unsettled = container.querySelectorAll(
    ".persona-stream-char[data-glyph-cycle-final]"
  );
  let seenSelf = false;
  for (const other of Array.from(unsettled)) {
    if (other === span) {
      seenSelf = true;
      continue;
    }
    if (!seenSelf) continue;
    if (Math.random() < CROSS_FLICKER_PROBABILITY) {
      other.textContent = randomGlyph();
    }
  }
};
var containerNextSettleAt = /* @__PURE__ */ new WeakMap();
var DEFAULT_ARRIVAL_MS = 25;
var STAGGER_DRAIN_FACTOR = 0.5;
var MIN_STAGGER_MS = 6;
var EMA_ALPHA = 0.25;
var containerLastScheduleAt = /* @__PURE__ */ new WeakMap();
var containerArrivalMs = /* @__PURE__ */ new WeakMap();
var observeStagger = (container, now) => {
  var _a;
  if (!container) return DEFAULT_ARRIVAL_MS * STAGGER_DRAIN_FACTOR;
  const last = containerLastScheduleAt.get(container);
  containerLastScheduleAt.set(container, now);
  let observed = (_a = containerArrivalMs.get(container)) != null ? _a : DEFAULT_ARRIVAL_MS;
  if (last !== void 0) {
    const interval = now - last;
    if (interval > 1) {
      observed = observed * (1 - EMA_ALPHA) + interval * EMA_ALPHA;
      containerArrivalMs.set(container, observed);
    }
  }
  return Math.max(MIN_STAGGER_MS, observed * STAGGER_DRAIN_FACTOR);
};
var activeCyclesByMessage = /* @__PURE__ */ new Map();
var getMessageId = (span) => {
  var _a;
  const bubble = span.closest("[data-message-id]");
  return (_a = bubble == null ? void 0 : bubble.dataset.messageId) != null ? _a : null;
};
var incrementActive = (messageId) => {
  var _a;
  if (!messageId) return;
  activeCyclesByMessage.set(messageId, ((_a = activeCyclesByMessage.get(messageId)) != null ? _a : 0) + 1);
};
var decrementActive = (messageId) => {
  var _a;
  if (!messageId) return;
  const count = (_a = activeCyclesByMessage.get(messageId)) != null ? _a : 0;
  if (count <= 1) activeCyclesByMessage.delete(messageId);
  else activeCyclesByMessage.set(messageId, count - 1);
};
var scheduleCycle = (span) => {
  var _a;
  if (span.dataset.glyphCycleScheduled === "true") return;
  const finalChar = (_a = span.textContent) != null ? _a : "";
  if (!finalChar || /\s/.test(finalChar)) return;
  span.dataset.glyphCycleScheduled = "true";
  span.dataset.glyphCycleFinal = finalChar;
  span.setAttribute("data-preserve-runtime", "stream-glyph-cycle");
  span.textContent = randomGlyph();
  const messageId = getMessageId(span);
  if (messageId) span.dataset.glyphCycleMessageId = messageId;
  incrementActive(messageId);
  const container = span.closest(".persona-stream-glyph-cycle");
  const now = Date.now();
  const staggerMs = observeStagger(container, now);
  const tickMs = getTickMs(span);
  const baseDurationMs = TICK_COUNT * tickMs;
  let settleAt = now + baseDurationMs;
  if (container) {
    const prev = containerNextSettleAt.get(container);
    if (prev !== void 0) settleAt = Math.max(settleAt, prev);
    containerNextSettleAt.set(container, settleAt + staggerMs);
  }
  startCycle(span, finalChar, settleAt);
};
var startCycle = (span, finalChar, settleAt) => {
  var _a;
  if (span.dataset.glyphCycleStarted === "true") return;
  span.dataset.glyphCycleStarted = "true";
  const tickMs = getTickMs(span);
  let lastGlyph = (_a = span.textContent) != null ? _a : void 0;
  const step = () => {
    var _a2;
    if (!span.isConnected) return;
    if (Date.now() >= settleAt) {
      span.textContent = finalChar;
      span.removeAttribute("data-preserve-runtime");
      delete span.dataset.glyphCycleStarted;
      delete span.dataset.glyphCycleFinal;
      decrementActive((_a2 = span.dataset.glyphCycleMessageId) != null ? _a2 : null);
      delete span.dataset.glyphCycleMessageId;
      return;
    }
    const glyph = randomGlyph(lastGlyph);
    span.textContent = glyph;
    lastGlyph = glyph;
    flickerLaterSiblings(span);
    setTimeout(step, tickMs);
  };
  setTimeout(step, tickMs);
};
var processCharSpans = (root) => {
  var _a;
  const spans = (_a = root.querySelectorAll) == null ? void 0 : _a.call(
    root,
    ".persona-stream-glyph-cycle .persona-stream-char:not([data-glyph-cycle-scheduled])"
  );
  if (!spans) return;
  for (const span of Array.from(spans)) {
    scheduleCycle(span);
  }
};
var isElement = (node) => node.nodeType === 1;
var glyphCycle = {
  name: "glyph-cycle",
  containerClass: "persona-stream-glyph-cycle",
  wrap: "char",
  // Narrow the default skip list so inline `<code>` and fenced `<pre>`
  // code blocks both render as cycling glyphs along with everything else.
  // Links stay clickable; <script>/<style> stay untouched.
  skipTags: ["a", "script", "style"],
  styles: STYLES,
  bufferContent(content) {
    if (content.length < BUFFER_THRESHOLD) return "";
    let boldPairs = 0;
    let lastSafe = -1;
    let i = 0;
    while (i < content.length) {
      if (content[i] === "*" && content[i + 1] === "*") {
        boldPairs += 1;
        i += 2;
        continue;
      }
      if (/\s/.test(content[i]) && boldPairs % 2 === 0) {
        lastSafe = i;
      }
      i += 1;
    }
    if (lastSafe < 0) return "";
    return content.slice(0, lastSafe);
  },
  isAnimating(message) {
    var _a;
    return ((_a = activeCyclesByMessage.get(message.id)) != null ? _a : 0) > 0;
  },
  onAttach(root) {
    processCharSpans(root);
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of Array.from(mutation.addedNodes)) {
          if (!isElement(node)) continue;
          if (node.classList.contains("persona-stream-char") && node.closest(".persona-stream-glyph-cycle")) {
            scheduleCycle(node);
          } else {
            processCharSpans(node);
          }
        }
      }
    });
    observer.observe(root, { childList: true, subtree: true });
    return () => observer.disconnect();
  }
};
registerStreamAnimationPlugin(glyphCycle);
var glyph_cycle_default = glyphCycle;
export {
  glyph_cycle_default as default,
  glyphCycle
};
