{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"message-actions","type":"registry:ui","title":"Message Actions","description":"The quiet bar under an AI message – copy turns into a check, regenerate spins and unfolds a version switcher, thumbs pop and ask why, read aloud becomes a tiny equalizer","dependencies":[],"files":[{"path":"registry/message-actions/message-actions.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { Fragment, useEffect, useId, useRef, useState, type KeyboardEvent, type ReactNode, type Ref } from \"react\";\n\n/*\n * Message Actions – the small bar under an AI chat message, in one self-contained file.\n *   · Copy: the icon turns into a check and the tooltip says \"Copied\"\n *   · Regenerate: the arrow spins once; with more than one version a \"‹ 2 / 3 ›\" switcher unfolds next to it\n *   · Thumbs up / down fill with a little pop; thumbs down unfolds a short feedback row with reasons and a send button\n *   · Read aloud: the speaker becomes a tiny equalizer while it plays, a second press stops it\n * The bar stays quiet (faded) until the message is hovered or something inside it is focused.\n * MessageText (also exported) crossfades the message itself when the version changes.\n * The styles are injected below, so no extra files are needed.\n */\n\nexport type MessageActionKey = \"copy\" | \"regenerate\" | \"feedback\" | \"speak\";\nexport type FeedbackKind = \"up\" | \"down\";\n\nexport type MessageActionsProps = {\n  /** The message text – copied to the clipboard by Copy and used to time Read aloud */\n  text?: string;\n  /** Which actions to show, in this order */\n  actions?: MessageActionKey[];\n  /** All versions of the message, or just how many there are. From 2 on the version switcher shows */\n  versions?: number | string[];\n  /** Controlled version (0-based index) */\n  version?: number;\n  /** Starting version when uncontrolled – defaults to the newest */\n  defaultVersion?: number;\n  /** Called with the version index picked in the switcher */\n  onVersionChange?: (index: number) => void;\n  /** Called after Copy, with `text` */\n  onCopy?: (text?: string) => void;\n  /** Called when Regenerate is pressed – add a version and the switcher jumps to it */\n  onRegenerate?: () => void;\n  /** Keeps the regenerate arrow spinning while a new version is written */\n  regenerating?: boolean;\n  /** \"up\" or \"down\" when a thumb is pressed, null when it is taken back, and \"down\" with a reason when the feedback row is sent */\n  onFeedback?: (kind: FeedbackKind | null, reason?: string) => void;\n  /** Quick reasons in the feedback row. \"Other\" opens a small text field */\n  reasons?: string[];\n  /** Called with true when Read aloud starts and false when it stops */\n  onSpeak?: (playing: boolean) => void;\n  /** Controlled playing state, e.g. tied to your speech player */\n  speaking?: boolean;\n  /** How long an uncontrolled read aloud plays, in ms – defaults to an estimate from `text` */\n  speakDuration?: number;\n  /** Faded until the message is hovered (default true). Put data-ui-message on the message wrapper */\n  quiet?: boolean;\n  /** Accessible name of the bar */\n  label?: string;\n  className?: string;\n};\n\n/* ---------- Helpers ---------- */\n\nconst reduced = () =>\n  typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches;\n\n/** Text that changes without a jump: the old one fades out underneath while the new one fades in */\nfunction Swap({ id, children, className }: { id: string; children: ReactNode; className?: string }) {\n  type Line = { id: string; node: ReactNode };\n  const last = useRef<Line>({ id, node: children });\n  const [shownId, setShownId] = useState(id);\n  const [out, setOut] = useState<Line | null>(null);\n  if (shownId !== id) {\n    setShownId(id);\n    setOut(last.current);\n  }\n  last.current = { id, node: children };\n  useEffect(() => {\n    if (!out) return;\n    const timer = window.setTimeout(() => setOut(null), 300);\n    return () => window.clearTimeout(timer);\n  }, [out]);\n  return (\n    <span className={[\"ui-ma__swap\", className].filter(Boolean).join(\" \")}>\n      {out && (\n        <span key={`out-${out.id}`} className=\"ui-ma__swap-item\" data-out=\"\" aria-hidden=\"true\">\n          {out.node}\n        </span>\n      )}\n      <span key={id} className=\"ui-ma__swap-item\">\n        {children}\n      </span>\n    </span>\n  );\n}\n\n/**\n * The message itself, crossfading when `version` changes: the old text fades out underneath while the new one fades in,\n * and the block eases to its new height.\n */\nexport function MessageText({ version, children, className }: { version: string | number; children: ReactNode; className?: string }) {\n  const inner = useRef<HTMLDivElement>(null);\n  const [height, setHeight] = useState<number | undefined>(undefined);\n  useEffect(() => {\n    const el = inner.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(() => setHeight(el.offsetHeight));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n  return (\n    <div className={[\"ui-ma-text\", className].filter(Boolean).join(\" \")} style={{ height }}>\n      <style href=\"ui-message-actions\" precedence=\"default\">\n        {css}\n      </style>\n      <div ref={inner} className=\"ui-ma-text__inner\" dir=\"auto\">\n        <Swap id={String(version)} className=\"ui-ma-text__swap\">\n          {children}\n        </Swap>\n      </div>\n    </div>\n  );\n}\n\n/* ---------- Icons ---------- */\n\nconst Glyph = ({ d }: { d: string }) => (\n  <svg viewBox=\"0 0 24 24\" className=\"ui-ma__glyph\" aria-hidden=\"true\">\n    <path d={d} />\n  </svg>\n);\nconst icons = {\n  copy: \"M9 9.5A1.5 1.5 0 0 1 10.5 8h8A1.5 1.5 0 0 1 20 9.5v9a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 9 18.5zM15 8V5.5A1.5 1.5 0 0 0 13.5 4h-8A1.5 1.5 0 0 0 4 5.5v9A1.5 1.5 0 0 0 5.5 16H9\",\n  check: \"M5.5 12.5l4.2 4.2L18.5 7.8\",\n  redo: \"M19.5 12a7.5 7.5 0 1 1-2.2-5.3L19.5 9M19.5 4.5V9H15\",\n  thumb: \"M7.5 10.5v9M7.5 10.5l3.3-6a1.8 1.8 0 0 1 3.3 1.2l-.6 3.8H18a2 2 0 0 1 2 2.3l-1.2 6.4a2 2 0 0 1-2 1.8H5a1 1 0 0 1-1-1v-7.5a1 1 0 0 1 1-1z\",\n  speaker: \"M11 5.5 6.8 9H4v6h2.8l4.2 3.5zM15.2 9.2a4 4 0 0 1 0 5.6M17.9 6.6a7.8 7.8 0 0 1 0 10.8\",\n  prev: \"m14.5 7-5 5 5 5\",\n  next: \"m9.5 7 5 5-5 5\",\n  close: \"m7.5 7.5 9 9m0-9-9 9\",\n};\n\nconst DEFAULT_ACTIONS: MessageActionKey[] = [\"copy\", \"regenerate\", \"feedback\", \"speak\"];\n/** Known actions only, each once – an unknown key or a repeat would render a stray or doubled button */\nconst cleanActions = (list: unknown): MessageActionKey[] =>\n  Array.isArray(list) ? [...new Set(list)].filter((k): k is MessageActionKey => DEFAULT_ACTIONS.includes(k)) : DEFAULT_ACTIONS;\n/** A whole, finite number or the fallback */\nconst whole = (n: unknown, fallback: number) => (typeof n === \"number\" && Number.isFinite(n) ? Math.floor(n) : fallback);\nconst fmt = (n: number) => n.toLocaleString(\"en-US\");\nconst DEFAULT_REASONS = [\"Not accurate\", \"Too long\", \"Didn't follow instructions\", \"Other\"];\n\nexport function MessageActions({\n  text,\n  actions: actionsProp = DEFAULT_ACTIONS,\n  versions,\n  version,\n  defaultVersion,\n  onVersionChange,\n  onCopy,\n  onRegenerate,\n  regenerating = false,\n  onFeedback,\n  reasons = DEFAULT_REASONS,\n  onSpeak,\n  speaking,\n  speakDuration,\n  quiet = true,\n  label = \"Message actions\",\n  className,\n}: MessageActionsProps) {\n  const uid = useId();\n  const actions = cleanActions(actionsProp);\n  const count = Math.max(1, whole(typeof versions === \"number\" ? versions : Array.isArray(versions) ? versions.length : 1, 1));\n\n  /* ----- Versions ----- */\n  const [inner, setInner] = useState(() => Math.min(count - 1, Math.max(0, whole(defaultVersion, count - 1))));\n  const [seenCount, setSeenCount] = useState(count);\n  const [vote, setVote] = useState<FeedbackKind | null>(null);\n  const [panel, setPanel] = useState(false);\n  const [reason, setReason] = useState<string | null>(null);\n  const [other, setOther] = useState(\"\");\n  const [sent, setSent] = useState(false);\n  if (seenCount !== count) {\n    // A new version arrived: show it, and its vote starts fresh\n    if (count > seenCount) {\n      setInner(count - 1);\n      setVote(null);\n      setPanel(false);\n    }\n    setSeenCount(count);\n  }\n  const index = Math.min(count - 1, Math.max(0, whole(version, inner)));\n  const goTo = (i: number) => {\n    const next = Math.min(count - 1, Math.max(0, i));\n    if (next === index) return;\n    if (version === undefined) setInner(next);\n    onVersionChange?.(next);\n    announce(`Version ${fmt(next + 1)} of ${fmt(count)}`);\n  };\n\n  /* ----- Status line for screen readers ----- */\n  const [status, setStatus] = useState(\"\");\n  const announce = (s: string) => setStatus((old) => (old === s ? `${s} ` : s));\n\n  /* ----- Copy ----- */\n  const [copied, setCopied] = useState(0);\n  useEffect(() => {\n    if (!copied) return;\n    const t = window.setTimeout(() => setCopied(0), 1600);\n    return () => window.clearTimeout(t);\n  }, [copied]);\n  const copy = () => {\n    try {\n      if (text !== undefined) navigator.clipboard?.writeText(text).catch(() => {});\n    } catch {\n      // No clipboard here (insecure page, old browser) – the check still confirms the press\n    }\n    onCopy?.(text);\n    setCopied(Date.now());\n    announce(\"Copied\");\n  };\n\n  /* ----- Regenerate ----- */\n  const [spin, setSpin] = useState(0);\n  const regenerate = () => {\n    setSpin((s) => s + 1);\n    onRegenerate?.();\n    announce(\"Regenerating\");\n  };\n\n  /* ----- Feedback ----- */\n  const thumbsUp = () => {\n    if (vote === \"up\") {\n      setVote(null);\n      onFeedback?.(null);\n      return;\n    }\n    setVote(\"up\");\n    setPanel(false);\n    onFeedback?.(\"up\");\n    announce(\"Marked as good response\");\n  };\n  const thumbsDown = () => {\n    if (vote === \"down\") {\n      setVote(null);\n      setPanel(false);\n      onFeedback?.(null);\n      return;\n    }\n    setVote(\"down\");\n    setPanel(true);\n    setSent(false);\n    setReason(null);\n    setOther(\"\");\n    onFeedback?.(\"down\");\n    announce(\"Marked as bad response. Tell us what went wrong below\");\n  };\n  const send = () => {\n    if (!reason || sent) return;\n    onFeedback?.(\"down\", reason === \"Other\" ? other.trim() || \"Other\" : reason);\n    setSent(true);\n    announce(\"Thanks for the feedback\");\n  };\n  useEffect(() => {\n    if (!sent) return;\n    const t = window.setTimeout(() => setPanel(false), reduced() ? 1200 : 1500);\n    return () => window.clearTimeout(t);\n  }, [sent]);\n  const downRef = useRef<HTMLButtonElement>(null);\n  const closePanel = () => {\n    setPanel(false);\n    downRef.current?.focus();\n  };\n\n  /* ----- Read aloud ----- */\n  const [innerSpeaking, setInnerSpeaking] = useState(false);\n  const playing = speaking ?? innerSpeaking;\n  const words = text ? text.trim().split(/\\s+/).length : 0;\n  const duration = typeof speakDuration === \"number\" && speakDuration > 0 ? speakDuration : Math.max(2600, words * 380);\n  useEffect(() => {\n    if (speaking !== undefined || !innerSpeaking) return;\n    const t = window.setTimeout(() => {\n      setInnerSpeaking(false);\n      onSpeak?.(false);\n      announce(\"Finished reading\");\n    }, duration);\n    return () => window.clearTimeout(t);\n    // onSpeak is read when the timer ends – a new function each render must not restart it\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [innerSpeaking, speaking, duration]);\n  const speak = () => {\n    const next = !playing;\n    if (speaking === undefined) setInnerSpeaking(next);\n    onSpeak?.(next);\n    announce(next ? \"Reading aloud\" : \"Stopped reading\");\n  };\n\n  const active = Boolean(copied) || panel || playing || regenerating;\n\n  /* ----- One button with its tooltip ----- */\n  const button = (o: {\n    key: string;\n    tip: string;\n    labelText: string;\n    onClick: () => void;\n    children: ReactNode;\n    pressed?: boolean;\n    flash?: boolean;\n    extra?: Record<string, string | undefined>;\n    expanded?: boolean;\n    controls?: string;\n    refEl?: Ref<HTMLButtonElement>;\n  }) => (\n    <span className=\"ui-ma__item\" key={o.key} data-flash={o.flash ? \"\" : undefined}>\n      <button\n        ref={o.refEl}\n        type=\"button\"\n        className={`ui-ma__btn ui-ma__btn--${o.key}`}\n        aria-label={o.labelText}\n        aria-pressed={o.pressed}\n        aria-expanded={o.expanded}\n        aria-controls={o.controls}\n        onClick={o.onClick}\n        {...o.extra}\n      >\n        {o.children}\n      </button>\n      <span className=\"ui-ma__tip\" aria-hidden=\"true\">\n        <Swap id={o.tip}>{o.tip}</Swap>\n      </span>\n    </span>\n  );\n\n  const parts: ReactNode[] = actions.map((key) => {\n    if (key === \"copy\")\n      return button({\n        key: \"copy\",\n        tip: copied ? \"Copied\" : \"Copy\",\n        labelText: copied ? \"Copied\" : \"Copy message\",\n        onClick: copy,\n        flash: Boolean(copied),\n        extra: { \"data-done\": copied ? \"\" : undefined },\n        children: (\n          <span className=\"ui-ma__icon ui-ma__morph\">\n            <span className=\"ui-ma__layer\" data-on={copied ? undefined : \"\"}>\n              <Glyph d={icons.copy} />\n            </span>\n            <span className=\"ui-ma__layer ui-ma__layer--check\" data-on={copied ? \"\" : undefined}>\n              <Glyph d={icons.check} />\n            </span>\n          </span>\n        ),\n      });\n    if (key === \"regenerate\")\n      return (\n        <span className=\"ui-ma__regen\" key=\"regenerate\">\n          {button({\n            key: \"regenerate\",\n            tip: regenerating ? \"Writing…\" : \"Regenerate\",\n            labelText: \"Regenerate response\",\n            onClick: regenerate,\n            extra: { \"data-busy\": regenerating ? \"\" : undefined },\n            children: (\n              <span className=\"ui-ma__icon\">\n                <span className=\"ui-ma__spin\" key={spin} data-spin={spin ? \"\" : undefined}>\n                  <Glyph d={icons.redo} />\n                </span>\n              </span>\n            ),\n          })}\n          <span className=\"ui-ma__versions\" data-open={count > 1 ? \"\" : undefined} inert={count < 2}>\n            <span className=\"ui-ma__versions-clip\" role=\"group\" aria-label=\"Versions\">\n              <button\n                type=\"button\"\n                className=\"ui-ma__step\"\n                data-dir=\"prev\"\n                aria-label=\"Previous version\"\n                disabled={index === 0}\n                onClick={() => goTo(index - 1)}\n              >\n                <Glyph d={icons.prev} />\n              </button>\n              <span className=\"ui-ma__count\" aria-label={`Version ${fmt(index + 1)} of ${fmt(count)}`}>\n                <Swap id={String(index)} className=\"ui-ma__count-now\">\n                  {fmt(index + 1)}\n                </Swap>\n                <span aria-hidden=\"true\">/</span>\n                <Swap id={String(count)}>{fmt(count)}</Swap>\n              </span>\n              <button\n                type=\"button\"\n                className=\"ui-ma__step\"\n                data-dir=\"next\"\n                aria-label=\"Next version\"\n                disabled={index >= count - 1}\n                onClick={() => goTo(index + 1)}\n              >\n                <Glyph d={icons.next} />\n              </button>\n            </span>\n          </span>\n        </span>\n      );\n    if (key === \"feedback\")\n      return (\n        <Fragment key=\"feedback\">\n          {button({\n          key: \"up\",\n          tip: vote === \"up\" ? \"Thanks!\" : \"Good response\",\n          labelText: \"Good response\",\n          pressed: vote === \"up\",\n          onClick: thumbsUp,\n          children: (\n            <span className=\"ui-ma__icon ui-ma__thumb\">\n              <Glyph d={icons.thumb} />\n            </span>\n          ),\n        })}\n        {button({\n          key: \"down\",\n          tip: \"Bad response\",\n          labelText: \"Bad response\",\n          pressed: vote === \"down\",\n          expanded: panel,\n          controls: `${uid}-feedback`,\n          onClick: thumbsDown,\n          refEl: downRef,\n          children: (\n            <span className=\"ui-ma__icon ui-ma__thumb\">\n              <Glyph d={icons.thumb} />\n            </span>\n          ),\n        })}\n        </Fragment>\n      );\n    return button({\n      key: \"speak\",\n      tip: playing ? \"Stop\" : \"Read aloud\",\n      labelText: playing ? \"Stop reading\" : \"Read aloud\",\n      pressed: playing,\n      onClick: speak,\n      children: (\n        <span className=\"ui-ma__icon ui-ma__morph\">\n          <span className=\"ui-ma__layer\" data-on={playing ? undefined : \"\"}>\n            <Glyph d={icons.speaker} />\n          </span>\n          <span className=\"ui-ma__layer ui-ma__eq\" data-on={playing ? \"\" : undefined}>\n            <i />\n            <i />\n            <i />\n            <i />\n          </span>\n        </span>\n      ),\n    });\n  });\n\n  const onPanelKey = (e: KeyboardEvent) => {\n    if (e.key === \"Escape\") {\n      e.stopPropagation();\n      closePanel();\n    }\n  };\n\n  return (\n    <div\n      className={[\"ui-ma\", className].filter(Boolean).join(\" \")}\n      data-quiet={quiet ? \"\" : undefined}\n      data-active={active ? \"\" : undefined}\n    >\n      <style href=\"ui-message-actions\" precedence=\"default\">\n        {css}\n      </style>\n\n      <div className=\"ui-ma__bar\" role=\"group\" aria-label={label}>\n        {parts}\n      </div>\n\n      {actions.includes(\"feedback\") && (\n        <div id={`${uid}-feedback`} className=\"ui-ma__fold\" data-open={panel ? \"\" : undefined} inert={!panel}>\n          <div className=\"ui-ma__clip\">\n            <div className=\"ui-ma__panel\" role=\"group\" aria-labelledby={`${uid}-ask`} onKeyDown={onPanelKey}>\n              <div className=\"ui-ma__panel-head\">\n                <span id={`${uid}-ask`} className=\"ui-ma__ask\">\n                  What went wrong?\n                </span>\n                <button type=\"button\" className=\"ui-ma__close\" aria-label=\"Close feedback\" onClick={closePanel}>\n                  <Glyph d={icons.close} />\n                </button>\n              </div>\n              <div className=\"ui-ma__chips\">\n                {(Array.isArray(reasons) ? reasons : []).map((r, i) => (\n                  <button\n                    key={`${r}-${i}`}\n                    type=\"button\"\n                    className=\"ui-ma__chip\"\n                    aria-pressed={reason === r}\n                    disabled={sent}\n                    onClick={() => setReason(reason === r ? null : r)}\n                    title={r}\n                    dir=\"auto\"\n                  >\n                    {r}\n                  </button>\n                ))}\n                <button\n                  type=\"button\"\n                  className=\"ui-ma__send\"\n                  data-sent={sent ? \"\" : undefined}\n                  disabled={!reason}\n                  aria-disabled={sent || undefined}\n                  onClick={send}\n                >\n                  <Swap id={sent ? \"sent\" : \"send\"}>\n                    {sent ? (\n                      <>\n                        <Glyph d={icons.check} />\n                        Thanks!\n                      </>\n                    ) : (\n                      \"Send\"\n                    )}\n                  </Swap>\n                </button>\n              </div>\n              {Array.isArray(reasons) && reasons.includes(\"Other\") && (\n                <div className=\"ui-ma__fold\" data-open={reason === \"Other\" && !sent ? \"\" : undefined} inert={reason !== \"Other\" || sent}>\n                  <div className=\"ui-ma__clip\">\n                    <input\n                      className=\"ui-ma__input\"\n                      value={other}\n                      placeholder=\"Tell us more (optional)\"\n                      dir=\"auto\"\n                      aria-label=\"What went wrong, in your words\"\n                      onChange={(e) => setOther(e.target.value)}\n                      onKeyDown={(e) => {\n                        if (e.key === \"Enter\") send();\n                      }}\n                    />\n                  </div>\n                </div>\n              )}\n            </div>\n          </div>\n        </div>\n      )}\n\n      <span className=\"ui-ma__sr\" aria-live=\"polite\">\n        {status}\n      </span>\n    </div>\n  );\n}\n\nconst css = /* css */ `\n.ui-ma {\n  --ui-ma-card: var(--aui-surface, #ffffff);\n  --ui-ma-line: #ececec;\n  --ui-ma-ink: var(--aui-ink, #171717);\n  --ui-ma-text: var(--aui-ink, #404040);\n  --ui-ma-muted: var(--aui-muted, #8f8f8f);\n  --ui-ma-well: var(--aui-subtle, #f4f4f5);\n  --ui-ma-hover: var(--aui-subtle, rgba(23, 23, 23, 0.06));\n  --ui-ma-accent: var(--aui-blue, #4fb0fe);\n  --ui-ma-accent-bg: color-mix(in srgb, var(--aui-blue, #0190fe) 10%, transparent);\n  --ui-ma-ok: var(--aui-success, #1e8a4f);\n  --ui-ma-ok-bg: color-mix(in srgb, var(--aui-green, #5fc88a) 11%, transparent);\n  --ui-ma-tip-bg: #171717;\n  --ui-ma-tip-text: #ffffff;\n  --ui-ma-focus: var(--aui-focus, rgba(0, 96, 226, 0.7));\n  --ui-ma-ease: cubic-bezier(0.32, 0.72, 0.24, 1);\n  box-sizing: border-box;\n  display: block;\n  width: 100%;\n  min-width: 0;\n  container-type: inline-size;\n  font-family: var(--aui-font, \"Inter\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  font-size: 13px;\n  line-height: 20px;\n  color: var(--ui-ma-text);\n  text-align: left;\n  -webkit-font-smoothing: antialiased;\n}\n.ui-ma *, .ui-ma *::before, .ui-ma *::after { box-sizing: border-box; }\n:where(.ui-ma) button { margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit; text-align: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; }\n.ui-ma button:focus-visible { outline: 2px solid var(--ui-ma-focus); outline-offset: 2px; }\n/* Typing in a field: no ring – the caret shows where you are, the edge only firms up a little */\n.ui-ma .ui-ma__input:is(:focus, :focus-visible) { outline: none; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-ma-ink) 14%, transparent); }\n.ui-ma svg { display: block; flex: none; }\n.ui-ma__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n\n/* ----- The bar: quiet until the message is hovered, something inside is focused or busy ----- */\n/* Wraps to a second row in a very narrow message instead of running out of it */\n.ui-ma__bar { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; min-height: 30px; transition: opacity 240ms ease; }\n.ui-ma[data-quiet] .ui-ma__bar { opacity: 0.6; }\n.ui-ma[data-quiet]:hover .ui-ma__bar,\n.ui-ma[data-quiet]:focus-within .ui-ma__bar,\n.ui-ma[data-quiet][data-active] .ui-ma__bar,\n:where([data-ui-message]:hover, [data-ui-message]:focus-within) .ui-ma[data-quiet] .ui-ma__bar { opacity: 1; }\n\n.ui-ma__item { position: relative; display: inline-flex; flex: none; }\n.ui-ma__btn { display: grid; place-items: center; width: 30px; height: 30px; border-radius: calc(var(--aui-radius-sm, 7px) + 1px); color: var(--ui-ma-muted); transition: background-color 160ms ease, color 160ms ease; }\n.ui-ma__btn:hover { background: var(--ui-ma-hover); color: var(--ui-ma-ink); }\n.ui-ma__btn:active .ui-ma__icon { transform: scale(0.9); }\n.ui-ma__btn[aria-pressed=\"true\"] { color: var(--ui-ma-ink); }\n.ui-ma__btn--speak[aria-pressed=\"true\"] { color: var(--ui-ma-accent); background: var(--ui-ma-accent-bg); }\n.ui-ma__btn--copy[data-done] { color: var(--ui-ma-ok); }\n\n.ui-ma__icon { position: relative; display: grid; place-items: center; width: 16px; height: 16px; transition: transform 160ms var(--ui-ma-ease); }\n.ui-ma__glyph { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }\n\n/* Two icons on top of each other – one blurs away while the other comes in */\n.ui-ma__morph { display: grid; }\n.ui-ma__layer { grid-area: 1 / 1; display: grid; place-items: center; opacity: 0; transform: scale(0.6); filter: blur(2px); transition: opacity 200ms ease, transform 300ms var(--ui-ma-ease), filter 200ms ease; }\n.ui-ma__layer[data-on] { opacity: 1; transform: none; filter: none; }\n.ui-ma__layer--check .ui-ma__glyph { stroke-width: 2; stroke-dasharray: 22; stroke-dashoffset: 22; }\n.ui-ma__layer--check[data-on] .ui-ma__glyph { stroke-dashoffset: 0; transition: stroke-dashoffset 340ms var(--ui-ma-ease) 60ms; }\n\n/* Regenerate spins once per press, and keeps going while busy */\n.ui-ma__spin { display: grid; place-items: center; }\n.ui-ma__spin[data-spin] { animation: ui-ma-spin 640ms var(--ui-ma-ease); }\n.ui-ma__btn[data-busy] { color: var(--ui-ma-ink); }\n.ui-ma__btn[data-busy] .ui-ma__spin { animation: ui-ma-spin 900ms linear infinite; }\n\n/* Thumbs: down is the same glyph turned around; a vote fills it with a little pop */\n.ui-ma__btn--down .ui-ma__glyph { transform: rotate(180deg); }\n.ui-ma__thumb .ui-ma__glyph { transition: fill 200ms ease; }\n.ui-ma__btn[aria-pressed=\"true\"] .ui-ma__thumb { animation: ui-ma-pop 420ms var(--ui-ma-ease); }\n.ui-ma__btn[aria-pressed=\"true\"] .ui-ma__thumb .ui-ma__glyph { fill: currentColor; }\n\n/* Read aloud: four bars that bounce while it plays */\n.ui-ma__eq { display: flex; align-items: center; justify-content: center; gap: 2px; width: 16px; height: 16px; }\n.ui-ma__eq i { display: block; width: 2px; height: 12px; border-radius: 1px; background: currentColor; transform: scaleY(0.3); }\n.ui-ma__eq[data-on] i { animation: ui-ma-eq 900ms ease-in-out infinite; }\n.ui-ma__eq[data-on] i:nth-child(2) { animation-delay: -300ms; animation-duration: 760ms; }\n.ui-ma__eq[data-on] i:nth-child(3) { animation-delay: -520ms; animation-duration: 1000ms; }\n.ui-ma__eq[data-on] i:nth-child(4) { animation-delay: -140ms; animation-duration: 820ms; }\n\n/* ----- Tooltips: above the button, on hover after a beat, on keyboard focus, and briefly after Copy ----- */\n.ui-ma__tip { position: absolute; bottom: calc(100% + 6px); left: 50%; z-index: 3; display: block; width: max-content; padding: 3px 7px; border-radius: 6px; background: var(--ui-ma-tip-bg); font-size: 11.5px; font-weight: 500; line-height: 16px; color: var(--ui-ma-tip-text); white-space: nowrap; pointer-events: none; opacity: 0; transform: translate(-50%, 3px); transition: opacity 140ms ease, transform 200ms var(--ui-ma-ease); }\n.ui-ma__btn:focus-visible + .ui-ma__tip,\n.ui-ma__item[data-flash] .ui-ma__tip { opacity: 1; transform: translate(-50%, 0); transition-delay: 0ms; }\n@media (hover: hover) {\n  .ui-ma__item:hover .ui-ma__tip { opacity: 1; transform: translate(-50%, 0); transition-delay: 380ms; }\n}\n\n/* ----- Version switcher: unfolds next to Regenerate once there are two versions ----- */\n.ui-ma__regen { display: inline-flex; align-items: center; flex: none; }\n.ui-ma__versions { display: grid; grid-template-columns: 0fr; opacity: 0; transition: grid-template-columns 420ms var(--ui-ma-ease), opacity 220ms ease; }\n.ui-ma__versions[data-open] { grid-template-columns: 1fr; opacity: 1; transition: grid-template-columns 420ms var(--ui-ma-ease), opacity 320ms ease 100ms; }\n.ui-ma__versions-clip { display: flex; align-items: center; min-width: 0; overflow: hidden; padding-right: 2px; }\n.ui-ma__step { display: grid; flex: none; place-items: center; width: 20px; height: 30px; border-radius: var(--aui-radius-sm, 7px); color: var(--ui-ma-muted); transition: background-color 160ms ease, color 160ms ease, opacity 160ms ease; }\n.ui-ma__step .ui-ma__glyph { width: 14px; height: 14px; }\n.ui-ma__step:hover:not(:disabled) { background: var(--ui-ma-hover); color: var(--ui-ma-ink); }\n.ui-ma__step:disabled { cursor: default; opacity: 0.4; }\n.ui-ma__count { display: inline-flex; align-items: center; gap: 2px; padding: 0 1px; font-size: 12px; color: var(--ui-ma-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }\n.ui-ma__count-now { color: var(--ui-ma-ink); font-weight: 500; }\n\n/* ----- Swapping text ----- */\n.ui-ma__swap { display: inline-grid; min-width: 0; }\n.ui-ma__swap-item { grid-area: 1 / 1; display: inline-flex; align-items: center; justify-content: center; gap: 4px; min-width: 0; animation: ui-ma-in 300ms ease both; }\n.ui-ma__swap-item[data-out] { animation: ui-ma-out 220ms ease both; }\n\n/* ----- Feedback row: unfolds below the bar ----- */\n.ui-ma__fold { display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows 420ms var(--ui-ma-ease), opacity 220ms ease; }\n.ui-ma__fold[data-open] { grid-template-rows: 1fr; opacity: 1; transition: grid-template-rows 420ms var(--ui-ma-ease), opacity 320ms ease 80ms; }\n.ui-ma__clip { min-height: 0; overflow: hidden; margin: -1px; padding: 1px; }  /* the 1px keeps the card's hairline from being clipped */\n.ui-ma__panel { max-width: 440px; margin: 6px 0 0; padding: 12px 14px 14px; border-radius: var(--aui-radius-lg, 16px); background: var(--ui-ma-card); box-shadow: var(--aui-shadow-card, 0 0 0 1px var(--aui-border, var(--ui-ma-line)), 0 1px 2px rgba(0, 0, 0, 0.04)); }\n.ui-ma__panel-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 24px; }\n.ui-ma__ask { font-size: 12.5px; font-weight: 500; color: var(--ui-ma-ink); }\n.ui-ma__close { display: grid; flex: none; place-items: center; width: 24px; height: 24px; margin-right: -4px; border-radius: var(--aui-radius-sm, 7px); color: var(--ui-ma-muted); transition: background-color 160ms ease, color 160ms ease; }\n.ui-ma__close .ui-ma__glyph { width: 14px; height: 14px; }\n.ui-ma__close:hover { background: var(--ui-ma-hover); color: var(--ui-ma-ink); }\n.ui-ma__chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 6px; }\n.ui-ma__chip { max-width: 100%; overflow: hidden; text-overflow: ellipsis; height: 26px; padding: 0 9px; border-radius: calc(var(--aui-radius-sm, 7px) + 1px); background: var(--ui-ma-well); font-size: 12px; color: var(--ui-ma-text); white-space: nowrap; transition: background-color 160ms ease, color 160ms ease, box-shadow 160ms ease, opacity 200ms ease; }\n.ui-ma__chip:hover:not(:disabled) { background: var(--ui-ma-line); color: var(--ui-ma-ink); }\n.ui-ma__chip[aria-pressed=\"true\"], .ui-ma__chip[aria-pressed=\"true\"]:hover:not(:disabled) { background: var(--ui-ma-accent-bg); color: var(--ui-ma-accent); }\n.ui-ma__chip:disabled { cursor: default; }\n.ui-ma__chip:disabled:not([aria-pressed=\"true\"]) { opacity: 0.45; }\n.ui-ma__send { display: inline-flex; align-items: center; justify-content: center; min-width: 58px; height: 26px; margin-left: auto; padding: 0 11px; border-radius: calc(var(--aui-radius-sm, 7px) + 1px); background: var(--aui-primary, var(--ui-ma-ink)); font-size: 12px; font-weight: 500; color: var(--aui-on-primary, var(--ui-ma-card)); white-space: nowrap; transition: background-color 220ms ease, color 220ms ease, opacity 200ms ease; }\n.ui-ma__send:disabled { cursor: default; opacity: 0.32; }\n.ui-ma__send[data-sent] { cursor: default; background: var(--ui-ma-ok-bg); color: var(--ui-ma-ok); }\n.ui-ma__send .ui-ma__glyph { width: 13px; height: 13px; stroke-width: 2.2; }\n.ui-ma__input { display: block; width: 100%; height: 30px; margin-top: 8px; padding: 0 10px; border: 0; border-radius: calc(var(--aui-radius, 10px) - 2px); background: var(--ui-ma-well); font: inherit; font-size: 12.5px; color: var(--ui-ma-ink); outline: none; }\n.ui-ma__input::placeholder { color: var(--aui-placeholder, var(--ui-ma-muted)); }\n\n/* ----- Message text that crossfades between versions (MessageText) ----- */\n.ui-ma-text { overflow: hidden; transition: height 360ms var(--ui-ma-ease, cubic-bezier(0.32, 0.72, 0.24, 1)); }\n.ui-ma-text__swap { display: grid; }\n.ui-ma-text__inner { min-width: 0; overflow-wrap: anywhere; }\n.ui-ma-text__swap > .ui-ma__swap-item { display: block; animation: ui-ma-text-in 420ms ease both; }\n.ui-ma-text__swap > .ui-ma__swap-item[data-out] { animation: ui-ma-out 260ms ease both; }\n\n@keyframes ui-ma-in { from { opacity: 0; filter: blur(2px); } }\n@keyframes ui-ma-text-in { from { opacity: 0; filter: blur(3px); } }\n@keyframes ui-ma-out { from { opacity: 1; } to { opacity: 0; filter: blur(2px); } }\n@keyframes ui-ma-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }\n@keyframes ui-ma-pop { 0% { transform: scale(1); } 40% { transform: scale(1.28) rotate(-8deg); } 100% { transform: scale(1); } }\n@keyframes ui-ma-eq { 0%, 100% { transform: scaleY(0.3); } 50% { transform: scaleY(1); } }\n\n/* Narrow: the feedback row stacks its send button under the chips */\n@container (max-width: 320px) {\n  .ui-ma__send { flex-basis: 100%; }\n}\n\n@media (pointer: coarse) {\n  .ui-ma[data-quiet] .ui-ma__bar { opacity: 1; }\n  .ui-ma__btn { width: 38px; height: 38px; border-radius: calc(var(--aui-radius-sm, 7px) + 3px); }\n  .ui-ma__step { width: 28px; height: 38px; }\n  .ui-ma__chip, .ui-ma__send { height: 34px; }\n  .ui-ma__close { width: 32px; height: 32px; }\n  .ui-ma__input { height: 38px; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .ui-ma *, .ui-ma *::before, .ui-ma *::after, .ui-ma-text, .ui-ma-text * { animation: none !important; transition: none !important; }\n  .ui-ma__eq[data-on] i:nth-child(odd) { transform: scaleY(0.8); }\n  .ui-ma__eq[data-on] i:nth-child(even) { transform: scaleY(0.5); }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> */\n:where(.dark, [data-theme=\"dark\"]) .ui-ma {\n  --ui-ma-card: var(--aui-surface, #19191b);\n  --ui-ma-line: #2a2a2e;\n  --ui-ma-ink: var(--aui-ink, #ededed);\n  --ui-ma-text: var(--aui-ink, #c8c8cc);\n  --ui-ma-muted: var(--aui-muted, #8b8b92);\n  --ui-ma-well: var(--aui-subtle, #222225);\n  --ui-ma-hover: var(--aui-subtle, rgba(255, 255, 255, 0.07));\n  --ui-ma-accent: #90cdfe;\n  --ui-ma-accent-bg: color-mix(in srgb, var(--aui-blue, #4fb0fe) 16%, transparent);\n  --ui-ma-ok: var(--aui-success, #7dcf9f);\n  --ui-ma-ok-bg: color-mix(in srgb, var(--aui-green, #7dcf9f) 12%, transparent);\n  --ui-ma-tip-bg: #ededed;\n  --ui-ma-tip-text: #171717;\n  --ui-ma-focus: var(--aui-focus, rgba(77, 141, 255, 0.8));\n}\n`;\n"}]}