{
  "name": "ai-message-queue",
  "type": "registry:component",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @kokonutui\n * @description: AI Message Queue\n * @version: 1.0.0\n * @date: 2026-10-21\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport {\n  ArrowUp,\n  Check,\n  CornerDownLeft,\n  GripVertical,\n  ListPlus,\n  Pencil,\n  Play,\n  Square,\n  X,\n} from \"lucide-react\";\nimport {\n  AnimatePresence,\n  MotionConfig,\n  motion,\n  Reorder,\n  useDragControls,\n} from \"motion/react\";\nimport {\n  type KeyboardEvent,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface AgentResponse {\n  /** What the agent shows while it works, one line per step. */\n  steps: string[];\n  reply: string;\n}\n\nexport interface QueuedMessage {\n  id: string;\n  text: string;\n}\n\nexport interface AIMessageQueueProps {\n  title?: string;\n  /** The message the agent is working on when the component mounts. */\n  initialPrompt?: string;\n  /** Follow-ups already waiting in the queue. */\n  initialQueue?: string[];\n  /** How the agent answers a message. Demo steps and replies when omitted. */\n  respond?: (text: string) => AgentResponse;\n  /** Milliseconds per step of a run. */\n  stepMs?: number;\n  className?: string;\n}\n\ninterface UserEntry {\n  id: string;\n  role: \"user\";\n  text: string;\n}\n\ninterface AgentEntry {\n  id: string;\n  role: \"agent\";\n  steps: string[];\n  reply: string;\n  step: number;\n  status: \"running\" | \"done\" | \"stopped\";\n  seconds: number;\n}\n\ntype Entry = UserEntry | AgentEntry;\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst APPLE = /Mac|iPhone|iPad/;\n\nconst DEMO: Record<string, AgentResponse> = {\n  \"Move session handling onto the new token store\": {\n    steps: [\n      \"Reading src/auth/session.ts\",\n      \"Finding every getSession call\",\n      \"Editing 6 files\",\n      \"Running auth tests\",\n    ],\n    reply:\n      \"Session handling now reads from TokenStore. I updated 6 files and the auth tests pass.\",\n  },\n  \"Also add a test for expired tokens\": {\n    steps: [\n      \"Opening session.test.ts\",\n      \"Writing the expired token case\",\n      \"Running auth tests\",\n    ],\n    reply:\n      \"Added a test that expires a token mid-request and checks we answer 401. 14 of 14 pass.\",\n  },\n  \"Keep the old cookie name for now\": {\n    steps: [\n      \"Searching for SESSION_COOKIE\",\n      \"Restoring kk_session\",\n      \"Running auth tests\",\n    ],\n    reply:\n      \"The cookie stays kk_session so nobody gets signed out. I left a TODO to rename it after the migration.\",\n  },\n  \"Then open a PR\": {\n    steps: [\n      \"Writing the PR description\",\n      \"Pushing auth/token-store\",\n      \"Opening the pull request\",\n    ],\n    reply:\n      \"Opened #482, Move sessions onto TokenStore, with the test and the cookie change in it.\",\n  },\n};\n\nconst demoRespond = (text: string): AgentResponse =>\n  DEMO[text] ?? {\n    steps: [\"Reading the thread\", \"Making the change\", \"Checking the result\"],\n    reply: \"Done, that's in. The tests still pass.\",\n  };\n\nlet counter = 0;\nconst uid = (prefix: string) => {\n  counter += 1;\n  return `${prefix}-${counter}`;\n};\n\nconst useModKey = () => {\n  const [mod, setMod] = useState(\"⌘\");\n  useEffect(() => {\n    if (!APPLE.test(navigator.userAgent)) {\n      setMod(\"Ctrl\");\n    }\n  }, []);\n  return mod;\n};\n\nconst startRun = (text: string, respond: (t: string) => AgentResponse) => {\n  const { steps, reply } = respond(text);\n  const user: UserEntry = { id: uid(\"u\"), role: \"user\", text };\n  const agent: AgentEntry = {\n    id: uid(\"a\"),\n    role: \"agent\",\n    steps,\n    reply,\n    step: 0,\n    status: \"running\",\n    seconds: 0,\n  };\n  return [user, agent] as const;\n};\n\nexport default function AIMessageQueue({\n  title = \"Auth refactor\",\n  initialPrompt = \"Move session handling onto the new token store\",\n  initialQueue = [\n    \"Also add a test for expired tokens\",\n    \"Keep the old cookie name for now\",\n  ],\n  respond = demoRespond,\n  stepMs = 1700,\n  className,\n}: AIMessageQueueProps) {\n  const [entries, setEntries] = useState<Entry[]>(() => [\n    { id: \"u-0\", role: \"user\", text: initialPrompt },\n    {\n      id: \"a-0\",\n      role: \"agent\",\n      ...respond(initialPrompt),\n      step: 0,\n      status: \"running\",\n      seconds: 0,\n    },\n  ]);\n  const [queue, setQueue] = useState<QueuedMessage[]>(() =>\n    initialQueue.map((text, i) => ({ id: `q-${i}`, text }))\n  );\n  const [paused, setPaused] = useState(false);\n  const [draft, setDraft] = useState(\"\");\n  const [announcement, setAnnouncement] = useState(\"\");\n  const mod = useModKey();\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n\n  const last = entries.at(-1);\n  const running = last?.role === \"agent\" && last.status === \"running\";\n\n  const runId = running ? last.id : null;\n  const runStep = running ? last.step : -1;\n\n  // Step the current run along.\n  useEffect(() => {\n    if (!runId || runStep < 0) {\n      return;\n    }\n    const id = setTimeout(() => {\n      setEntries((list) =>\n        list.map((e) => {\n          if (e.id !== runId || e.role !== \"agent\") {\n            return e;\n          }\n          const step = e.step + 1;\n          return step >= e.steps.length\n            ? { ...e, step, status: \"done\" }\n            : { ...e, step };\n        })\n      );\n    }, stepMs);\n    return () => clearTimeout(id);\n  }, [runId, runStep, stepMs]);\n\n  // A clock for the run, so \"Working · 4s\" keeps moving.\n  useEffect(() => {\n    if (!running) {\n      return;\n    }\n    const id = setInterval(() => {\n      setEntries((list) =>\n        list.map((e) =>\n          e.role === \"agent\" && e.status === \"running\"\n            ? { ...e, seconds: e.seconds + 1 }\n            : e\n        )\n      );\n    }, 1000);\n    return () => clearInterval(id);\n  }, [running]);\n\n  // When a run ends, the next queued message goes in.\n  useEffect(() => {\n    if (running || paused || queue.length === 0) {\n      return;\n    }\n    const id = setTimeout(() => {\n      const [next, ...rest] = queue;\n      setQueue(rest);\n      setEntries((list) => [...list, ...startRun(next.text, respond)]);\n    }, 450);\n    return () => clearTimeout(id);\n  }, [running, paused, queue, respond]);\n\n  // Keep the newest activity in view.\n  const entryCount = entries.length;\n  const lastStep = last?.role === \"agent\" ? last.step : 0;\n  useEffect(() => {\n    const el = scrollRef.current;\n    if (entryCount > 0 && el) {\n      el.scrollTo({ top: el.scrollHeight + lastStep, behavior: \"smooth\" });\n    }\n  }, [entryCount, lastStep]);\n\n  const stopCurrent = useCallback(() => {\n    setEntries((list) =>\n      list.map((e) =>\n        e.role === \"agent\" && e.status === \"running\"\n          ? { ...e, status: \"stopped\" }\n          : e\n      )\n    );\n  }, []);\n\n  /** Interrupts the run and sends this message straight away. */\n  const sendNow = (text: string) => {\n    setPaused(false);\n    setEntries((list) => [\n      ...list.map((e) =>\n        e.role === \"agent\" && e.status === \"running\"\n          ? { ...e, status: \"stopped\" as const }\n          : e\n      ),\n      ...startRun(text, respond),\n    ]);\n  };\n\n  const submit = (now: boolean) => {\n    const text = draft.trim();\n    if (!text) {\n      return;\n    }\n    setDraft(\"\");\n    if (running && !now) {\n      setQueue((q) => [...q, { id: uid(\"q\"), text }]);\n      return;\n    }\n    if (running) {\n      sendNow(text);\n      return;\n    }\n    setPaused(false);\n    setEntries((list) => [...list, ...startRun(text, respond)]);\n  };\n\n  const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n    // Enter that confirms an IME candidate must not send.\n    if (event.nativeEvent.isComposing) {\n      return;\n    }\n    if (event.key === \"Enter\" && !event.shiftKey) {\n      event.preventDefault();\n      submit(event.metaKey || event.ctrlKey);\n    }\n  };\n\n  // Remove, Clear and Send now take their row (or the whole list) away, so\n  // focus goes to the composer instead of dropping to the page.\n  // Only from the keyboard: a tap shouldn't pop the on-screen keyboard.\n  const toComposer = () => {\n    if (document.activeElement?.matches(\":focus-visible\")) {\n      inputRef.current?.focus({ preventScroll: true });\n    }\n  };\n\n  const queueing = running && draft.trim().length > 0;\n  let action: \"stop\" | \"queue\" | \"send\" = \"send\";\n  if (queueing) {\n    action = \"queue\";\n  } else if (running) {\n    action = \"stop\";\n  }\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div\n        className={cn(\n          \"flex w-full max-w-md flex-col rounded-2xl bg-white ring-1 ring-black/[0.08] dark:bg-zinc-900 dark:ring-white/10\",\n          className\n        )}\n      >\n        <header className=\"flex items-center gap-2 border-black/[0.06] border-b px-4 py-3 dark:border-white/[0.08]\">\n          <h3 className=\"min-w-0 flex-1 truncate font-medium text-[13px] text-zinc-900 dark:text-zinc-100\">\n            {title}\n          </h3>\n          <span\n            className={cn(\n              \"text-[12px] tabular-nums\",\n              running\n                ? \"text-zinc-600 dark:text-zinc-300\"\n                : \"text-zinc-400 dark:text-zinc-500\"\n            )}\n          >\n            {running ? \"Working\" : \"Idle\"}\n          </span>\n        </header>\n\n        <div\n          className=\"h-72 overflow-y-auto overscroll-contain px-4 py-4 [scrollbar-width:thin]\"\n          ref={scrollRef}\n        >\n          <div className=\"flex min-h-full flex-col justify-end gap-4\">\n            <AnimatePresence initial={false}>\n              {entries.map((entry) => (\n                <motion.div\n                  animate={{ opacity: 1, y: 0 }}\n                  initial={{ opacity: 0, y: 10 }}\n                  key={entry.id}\n                  transition={{ duration: 0.3, ease: EASE_OUT }}\n                >\n                  {entry.role === \"user\" ? (\n                    <div className=\"flex justify-end\">\n                      <p className=\"max-w-[85%] rounded-2xl rounded-br-md bg-zinc-100 px-3.5 py-2 text-[13px] text-zinc-800 leading-snug dark:bg-zinc-800 dark:text-zinc-100\">\n                        {entry.text}\n                      </p>\n                    </div>\n                  ) : (\n                    <AgentBlock entry={entry} />\n                  )}\n                </motion.div>\n              ))}\n            </AnimatePresence>\n          </div>\n        </div>\n\n        <div className=\"p-2 pt-0\">\n          <div className=\"rounded-xl bg-zinc-50 ring-1 ring-black/[0.07] dark:bg-zinc-800/60 dark:ring-white/[0.08]\">\n            <QueueList\n              onClear={() => {\n                setQueue([]);\n                toComposer();\n              }}\n              onEdit={(id, text) =>\n                setQueue((q) =>\n                  q.map((m) => (m.id === id ? { ...m, text } : m))\n                )\n              }\n              onMoved={(text, position) =>\n                setAnnouncement(`Moved \"${text}\" to position ${position}`)\n              }\n              onRemove={(id) => {\n                setQueue((q) => q.filter((m) => m.id !== id));\n                toComposer();\n              }}\n              onReorder={setQueue}\n              onResume={() => setPaused(false)}\n              onSendNow={(m) => {\n                setQueue((q) => q.filter((x) => x.id !== m.id));\n                sendNow(m.text);\n                toComposer();\n              }}\n              paused={paused && !running}\n              progress={\n                running && last.steps.length > 0\n                  ? (last.step + 1) / last.steps.length\n                  : 0\n              }\n              queue={queue}\n            />\n            <span aria-live=\"polite\" className=\"sr-only\">\n              {announcement}\n            </span>\n            <div className=\"flex items-end gap-2 p-2\">\n              <textarea\n                aria-label=\"Message the agent\"\n                className=\"max-h-28 min-h-8 flex-1 resize-none bg-transparent px-1.5 py-1.5 text-[13px] text-zinc-900 leading-5 outline-hidden placeholder:text-zinc-400 dark:text-zinc-100 dark:placeholder:text-zinc-500 [@media(pointer:coarse)]:text-base\"\n                onChange={(event) => setDraft(event.target.value)}\n                onKeyDown={onKeyDown}\n                placeholder={\n                  running\n                    ? \"Queue a follow-up…\"\n                    : \"Ask the agent to do something…\"\n                }\n                ref={inputRef}\n                rows={1}\n                value={draft}\n              />\n              <ComposerAction\n                draft={draft}\n                mode={action}\n                onSend={() => submit(false)}\n                onStop={() => {\n                  stopCurrent();\n                  setPaused(queue.length > 0);\n                }}\n              />\n            </div>\n          </div>\n          <p className=\"flex h-7 items-center gap-1 px-2 text-[11px] text-zinc-400 dark:text-zinc-500\">\n            {running ? (\n              <>\n                <CornerDownLeft aria-hidden=\"true\" className=\"size-3\" />\n                to queue\n                <span className=\"mx-1\">·</span>\n                <kbd className=\"font-sans\">{mod}</kbd>\n                <CornerDownLeft aria-hidden=\"true\" className=\"size-3\" />\n                to send now\n              </>\n            ) : (\n              \"The agent picks up queued messages when it finishes\"\n            )}\n          </p>\n        </div>\n      </div>\n    </MotionConfig>\n  );\n}\n\nfunction AgentBlock({ entry }: { entry: AgentEntry }) {\n  const working = entry.status === \"running\";\n  const current = entry.steps[Math.min(entry.step, entry.steps.length - 1)];\n  return (\n    <div className=\"space-y-1.5\">\n      <div className=\"flex h-5 items-center gap-2 text-[12px] text-zinc-500 dark:text-zinc-400\">\n        {working ? (\n          <>\n            <Pulse />\n            <span className=\"relative h-5 min-w-0 flex-1 overflow-hidden\">\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                <motion.span\n                  animate={{ y: 0, opacity: 1, filter: \"blur(0px)\" }}\n                  className=\"absolute inset-0 truncate leading-5\"\n                  exit={{ y: -10, opacity: 0, filter: \"blur(2px)\" }}\n                  initial={{ y: 10, opacity: 0, filter: \"blur(2px)\" }}\n                  key={entry.step}\n                  transition={{ duration: 0.25, ease: EASE_OUT }}\n                >\n                  {current}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n            <span className=\"shrink-0 text-zinc-400 tabular-nums dark:text-zinc-500\">\n              {entry.step + 1}/{entry.steps.length} · {entry.seconds}s\n            </span>\n          </>\n        ) : (\n          <>\n            {entry.status === \"done\" ? (\n              <Check aria-hidden=\"true\" className=\"size-3.5\" />\n            ) : (\n              <Square aria-hidden=\"true\" className=\"size-2.5 fill-current\" />\n            )}\n            <span>\n              {entry.status === \"done\"\n                ? `${entry.steps.length} steps · ${entry.seconds}s`\n                : `Stopped after ${entry.step} of ${entry.steps.length} steps`}\n            </span>\n          </>\n        )}\n      </div>\n      <AnimatePresence initial={false}>\n        {entry.status === \"done\" && (\n          <motion.p\n            animate={{ opacity: 1, filter: \"blur(0px)\" }}\n            className=\"text-[13px] text-zinc-800 leading-relaxed dark:text-zinc-200\"\n            initial={{ opacity: 0, filter: \"blur(3px)\" }}\n            transition={{ duration: 0.35, ease: EASE_OUT }}\n          >\n            {entry.reply}\n          </motion.p>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction ComposerAction({\n  mode,\n  draft,\n  onSend,\n  onStop,\n}: {\n  mode: \"stop\" | \"queue\" | \"send\";\n  draft: string;\n  onSend: () => void;\n  onStop: () => void;\n}) {\n  const swap = {\n    initial: { opacity: 0, filter: \"blur(2px)\" },\n    animate: { opacity: 1, filter: \"blur(0px)\" },\n    exit: { opacity: 0, filter: \"blur(2px)\" },\n    transition: { duration: 0.14, ease: EASE_OUT },\n  };\n  // The wrapper fades, so the button's own disabled opacity still shows.\n  return (\n    <AnimatePresence initial={false} mode=\"popLayout\">\n      <motion.span className=\"flex shrink-0\" key={mode} {...swap}>\n        {mode === \"stop\" ? (\n          <button\n            aria-label=\"Stop the agent\"\n            className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-zinc-200 text-zinc-700 transition-colors duration-150 hover:bg-zinc-300 active:bg-zinc-400/70 dark:bg-zinc-700 dark:text-zinc-200 dark:active:bg-zinc-500 dark:hover:bg-zinc-600\"\n            onClick={onStop}\n            type=\"button\"\n          >\n            <Square className=\"size-3 fill-current\" />\n          </button>\n        ) : (\n          <button\n            aria-label={mode === \"queue\" ? \"Add to queue\" : \"Send\"}\n            className=\"flex h-8 shrink-0 items-center gap-1.5 rounded-lg bg-zinc-900 px-2.5 font-medium text-[12px] text-white transition-[background-color,opacity] duration-150 hover:bg-zinc-700 active:bg-zinc-600 disabled:opacity-30 dark:bg-white dark:text-zinc-900 dark:active:bg-zinc-300 dark:hover:bg-zinc-200\"\n            disabled={!draft.trim()}\n            onClick={onSend}\n            type=\"button\"\n          >\n            {mode === \"queue\" ? (\n              <>\n                <ListPlus aria-hidden=\"true\" className=\"size-3.5\" />\n                Queue\n              </>\n            ) : (\n              <ArrowUp aria-hidden=\"true\" className=\"size-3.5\" />\n            )}\n          </button>\n        )}\n      </motion.span>\n    </AnimatePresence>\n  );\n}\n\nfunction QueueList({\n  queue,\n  paused,\n  progress,\n  onMoved,\n  onReorder,\n  onEdit,\n  onRemove,\n  onSendNow,\n  onClear,\n  onResume,\n}: {\n  queue: QueuedMessage[];\n  paused: boolean;\n  /** How far the current run is, 0 to 1. The next row in line shows it. */\n  progress: number;\n  onMoved: (text: string, position: number) => void;\n  onReorder: (queue: QueuedMessage[]) => void;\n  onEdit: (id: string, text: string) => void;\n  onRemove: (id: string) => void;\n  onSendNow: (message: QueuedMessage) => void;\n  onClear: () => void;\n  onResume: () => void;\n}) {\n  const move = (index: number, by: number) => {\n    const to = index + by;\n    if (to < 0 || to >= queue.length) {\n      return;\n    }\n    const next = [...queue];\n    const [item] = next.splice(index, 1);\n    next.splice(to, 0, item);\n    onReorder(next);\n    onMoved(item.text, to + 1);\n  };\n\n  return (\n    <AnimatePresence initial={false}>\n      {queue.length > 0 && (\n        <motion.div\n          animate={{ height: \"auto\", opacity: 1 }}\n          className=\"overflow-hidden\"\n          exit={{ height: 0, opacity: 0 }}\n          initial={{ height: 0, opacity: 0 }}\n          transition={{ duration: 0.25, ease: EASE_OUT }}\n        >\n          <div className=\"border-black/[0.06] border-b px-1.5 pt-1.5 pb-1 dark:border-white/[0.08]\">\n            <div className=\"flex h-6 items-center gap-2 px-1.5 text-[11px] text-zinc-500 dark:text-zinc-400\">\n              <span>\n                {paused ? \"Queue paused\" : \"Queued\"}\n                <span className=\"ml-1 tabular-nums\">{queue.length}</span>\n              </span>\n              {paused && (\n                <button\n                  className=\"flex items-center gap-1 rounded px-1 font-medium text-zinc-800 transition-colors duration-100 hover:bg-zinc-900/[0.06] dark:text-zinc-100 dark:hover:bg-white/10\"\n                  onClick={onResume}\n                  type=\"button\"\n                >\n                  <Play aria-hidden=\"true\" className=\"size-2.5 fill-current\" />\n                  Resume\n                </button>\n              )}\n              <button\n                className=\"ml-auto rounded px-1 transition-colors duration-100 hover:bg-zinc-900/[0.06] hover:text-zinc-800 dark:hover:bg-white/10 dark:hover:text-zinc-100\"\n                onClick={onClear}\n                type=\"button\"\n              >\n                Clear\n              </button>\n            </div>\n            <Reorder.Group\n              axis=\"y\"\n              className=\"space-y-0.5\"\n              onReorder={onReorder}\n              values={queue}\n            >\n              <AnimatePresence initial={false}>\n                {queue.map((message, index) => (\n                  <QueueRow\n                    index={index}\n                    key={message.id}\n                    message={message}\n                    next={index === 0}\n                    onEdit={(text) => onEdit(message.id, text)}\n                    onMove={(by) => move(index, by)}\n                    onRemove={() => onRemove(message.id)}\n                    onSendNow={() => onSendNow(message)}\n                    progress={paused ? 0 : progress}\n                  />\n                ))}\n              </AnimatePresence>\n            </Reorder.Group>\n          </div>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n\nfunction QueueRow({\n  message,\n  index,\n  next,\n  progress,\n  onEdit,\n  onMove,\n  onRemove,\n  onSendNow,\n}: {\n  message: QueuedMessage;\n  index: number;\n  next: boolean;\n  progress: number;\n  onEdit: (text: string) => void;\n  onMove: (by: number) => void;\n  onRemove: () => void;\n  onSendNow: () => void;\n}) {\n  const controls = useDragControls();\n  const [editing, setEditing] = useState(false);\n  const [value, setValue] = useState(message.text);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const editRef = useRef<HTMLButtonElement>(null);\n  /** Set once an edit ends, so the blur that follows can't save it again. */\n  const finished = useRef(false);\n\n  useEffect(() => {\n    if (editing) {\n      finished.current = false;\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    }\n  }, [editing]);\n\n  /** Ends the edit. Escape passes `save: false`; keyboard exits refocus Edit. */\n  const finish = (save: boolean, refocus: boolean) => {\n    if (finished.current) {\n      return;\n    }\n    finished.current = true;\n    const text = value.trim();\n    if (save && text) {\n      onEdit(text);\n    } else {\n      setValue(message.text);\n    }\n    setEditing(false);\n    if (refocus) {\n      editRef.current?.focus();\n    }\n  };\n\n  const icon =\n    \"flex size-6 shrink-0 items-center justify-center rounded-md text-zinc-400 outline-hidden transition-colors duration-100 hover:bg-zinc-900/[0.06] hover:text-zinc-800 focus-visible:bg-zinc-900/[0.06] active:bg-zinc-900/[0.1] dark:active:bg-white/15 focus-visible:text-zinc-800 dark:text-zinc-500 dark:focus-visible:bg-white/10 dark:focus-visible:text-zinc-100 dark:hover:bg-white/10 dark:hover:text-zinc-100\";\n\n  return (\n    <Reorder.Item\n      animate={{ opacity: 1, y: 0 }}\n      className=\"group/row relative flex h-8 items-center gap-1 overflow-hidden rounded-lg bg-white pr-1 pl-0.5 ring-1 ring-black/[0.05] dark:bg-zinc-900 dark:ring-white/[0.06]\"\n      dragControls={controls}\n      dragListener={false}\n      exit={{ opacity: 0, y: -6, transition: { duration: 0.15 } }}\n      initial={{ opacity: 0, y: 8 }}\n      transition={{ duration: 0.22, ease: EASE_OUT }}\n      value={message}\n      whileDrag={{\n        boxShadow: \"0 8px 20px -8px rgb(0 0 0 / 0.25)\",\n        zIndex: 10,\n      }}\n    >\n      <button\n        aria-label={`Move \"${message.text}\". Use the arrow keys to reorder`}\n        className={cn(icon, \"cursor-grab touch-none active:cursor-grabbing\")}\n        onKeyDown={(event) => {\n          if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n            event.preventDefault();\n            onMove(event.key === \"ArrowUp\" ? -1 : 1);\n          }\n        }}\n        onPointerDown={(event) => controls.start(event)}\n        type=\"button\"\n      >\n        <GripVertical className=\"size-3.5\" />\n      </button>\n      {next && (\n        <motion.span\n          animate={{ scaleX: progress, opacity: progress > 0 ? 1 : 0 }}\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-x-0 bottom-0 h-px origin-left bg-zinc-900/25 dark:bg-white/30\"\n          initial={false}\n          transition={{ duration: 0.4, ease: EASE_OUT }}\n        />\n      )}\n      <span className=\"w-3.5 shrink-0 text-center text-[11px] text-zinc-400 tabular-nums dark:text-zinc-500\">\n        {index + 1}\n      </span>\n      {editing ? (\n        <input\n          aria-label=\"Edit queued message\"\n          className=\"h-6 min-w-0 flex-1 rounded-md bg-zinc-100 px-1.5 text-[12px] text-zinc-900 outline-hidden dark:bg-zinc-800 dark:text-zinc-100 [@media(pointer:coarse)]:text-base\"\n          onBlur={() => finish(true, false)}\n          onChange={(event) => setValue(event.target.value)}\n          onKeyDown={(event) => {\n            if (event.nativeEvent.isComposing) {\n              return;\n            }\n            if (event.key === \"Enter\") {\n              event.preventDefault();\n              finish(true, true);\n            } else if (event.key === \"Escape\") {\n              event.preventDefault();\n              finish(false, true);\n            }\n          }}\n          ref={inputRef}\n          value={value}\n        />\n      ) : (\n        <p className=\"min-w-0 flex-1 truncate px-1.5 text-[12px] text-zinc-700 dark:text-zinc-200\">\n          {message.text}\n        </p>\n      )}\n      <div className=\"flex shrink-0 items-center opacity-100 transition-opacity duration-150 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:focus-within:opacity-100 [@media(hover:hover)]:group-hover/row:opacity-100\">\n        <button\n          aria-label=\"Edit\"\n          className={icon}\n          onClick={() => setEditing(true)}\n          ref={editRef}\n          type=\"button\"\n        >\n          <Pencil className=\"size-3\" />\n        </button>\n        <button\n          aria-label=\"Send now\"\n          className={icon}\n          onClick={onSendNow}\n          title=\"Stop the agent and send this now\"\n          type=\"button\"\n        >\n          <ArrowUp className=\"size-3.5\" />\n        </button>\n        <button\n          aria-label=\"Remove from queue\"\n          className={icon}\n          onClick={onRemove}\n          type=\"button\"\n        >\n          <X className=\"size-3.5\" />\n        </button>\n      </div>\n    </Reorder.Item>\n  );\n}\n\nfunction Pulse() {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"relative flex size-3.5 items-center justify-center\"\n    >\n      <motion.span\n        animate={{ opacity: [0.35, 1, 0.35] }}\n        className=\"size-1.5 rounded-full bg-zinc-800 dark:bg-zinc-200\"\n        transition={{\n          duration: 1.2,\n          repeat: Number.POSITIVE_INFINITY,\n          ease: \"easeInOut\",\n        }}\n      />\n    </span>\n  );\n}\n",
      "path": "/components/kokonutui/ai-message-queue.tsx",
      "target": "components/kokonutui/ai-message-queue.tsx"
    }
  ]
}