{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sluice",
  "type": "registry:ui",
  "title": "Sluice",
  "description": "Prompt composer for chat UIs — an auto-growing textarea in a glass capsule with spring-in attachment chips, an optional model pill, and a send disc that morphs into a stop button while a light laps the border during generation. Enter/Shift+Enter/Escape, IME-safe, file paste, and labelled controls.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://crucibleui.com/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/default/ai/sluice/sluice.tsx",
      "type": "registry:ui",
      "target": "components/crucible/sluice.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\n\n/** One file attached to the composer, rendered as a removable chip. */\nexport interface SluiceAttachment {\n  /** Stable, unique id — used as the React key and passed to `onRemoveAttachment`. */\n  id: string;\n  /** Filename shown on the chip and in its remove button's label. */\n  name: string;\n  /** Size in bytes. Rendered as a compact \"12 KB\" suffix when present. */\n  size?: number;\n}\n\nexport interface SluiceProps\n  extends Omit<\n    React.ComponentPropsWithoutRef<\"div\">,\n    \"onSubmit\" | \"defaultValue\" | \"onChange\"\n  > {\n  /** Controlled draft text. Pair with `onValueChange`. */\n  value?: string;\n  /** Initial draft text when uncontrolled. @default \"\" */\n  defaultValue?: string;\n  /** Fires with the next draft on every edit (and with `\"\"` after submit), controlled or not. */\n  onValueChange?: (value: string) => void;\n  /**\n   * Fires on Enter or the send button with the draft and the current\n   * attachments. The composer clears its own draft (and, when uncontrolled,\n   * its attachments) right after.\n   */\n  onSubmit?: (text: string, attachments: SluiceAttachment[]) => void;\n  /**\n   * The assistant is working. The send button morphs into a stop button, a\n   * light traces the capsule border, and Enter no longer submits.\n   * @default false\n   */\n  busy?: boolean;\n  /** Fires from the stop button or Escape while `busy`. */\n  onStop?: () => void;\n  /** Textarea placeholder. @default \"Ask anything…\" */\n  placeholder?: string;\n  /**\n   * Controlled attachment chips. Omit to let the composer track files picked\n   * or pasted itself (cleared on submit).\n   */\n  attachments?: SluiceAttachment[];\n  /** Fires with files chosen from the attach button or pasted from the clipboard. */\n  onAttach?: (files: File[]) => void;\n  /** Fires when a chip's remove button is pressed. */\n  onRemoveAttachment?: (id: string) => void;\n  /** Hide the attach button entirely. @default false */\n  hideAttach?: boolean;\n  /** Model names for the compact model pill. The pill is hidden when omitted or empty. */\n  models?: string[];\n  /** Controlled selected model. Pair with `onModelChange`. */\n  model?: string;\n  /** Initial model when uncontrolled. @default models[0] */\n  defaultModel?: string;\n  /** Fires when the model pill changes, controlled or not. */\n  onModelChange?: (model: string) => void;\n  /** Rows the textarea grows to before it scrolls. @default 8 */\n  maxRows?: number;\n  /** Accent for the busy border trace, stop button, and focus rings. @default \"#a78bfa\" (ion violet) */\n  accentColor?: string;\n  /** Accessible name for the textarea. @default \"Message\" */\n  label?: string;\n  /**\n   * Embedded variant for panels and side sheets: a tighter, flatter field —\n   * smaller radius, quiet fill, no glass blur or drop shadow, trimmed padding.\n   * @default false\n   */\n  compact?: boolean;\n}\n\nconst useIsoLayoutEffect =\n  typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\nfunction formatSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`;\n  const kb = bytes / 1024;\n  if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;\n  const mb = kb / 1024;\n  return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;\n}\n\nconst TEXT_CLASS =\n  \"w-full whitespace-pre-wrap break-words px-4 pt-3.5 pb-1.5 text-[15px] leading-6\";\nconst TEXT_CLASS_COMPACT =\n  \"w-full whitespace-pre-wrap break-words px-3.5 pt-3 pb-1 text-[15px] leading-6\";\n\n/**\n * Sluice — a prompt composer for chat UIs. An auto-growing textarea inside a\n * rounded glass capsule, with a toolbar of attach button, spring-in attachment\n * chips, an optional model pill, and a send button. While `busy`, the send\n * disc morphs into a rounded-square stop button and a thin accent light laps\n * the capsule border, so the input itself shows the system is working. On\n * submit the text lifts and fades upward as it clears.\n *\n * Keyboard: Enter submits, Shift+Enter inserts a newline, Escape stops while\n * busy; IME composition is respected. Pasting files attaches them.\n *\n * Accessibility: labelled textarea, send/stop button with a switching label,\n * chips as a list with labelled remove buttons, and a polite status when the\n * assistant starts responding. Honors prefers-reduced-motion: no border trace\n * (a static accent border instead), instant morphs, short opacity fades only.\n */\nexport function Sluice({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onSubmit,\n  busy = false,\n  onStop,\n  placeholder = \"Ask anything…\",\n  attachments,\n  onAttach,\n  onRemoveAttachment,\n  hideAttach = false,\n  models,\n  model,\n  defaultModel,\n  onModelChange,\n  maxRows = 8,\n  accentColor = \"#a78bfa\",\n  label = \"Message\",\n  compact = false,\n  className,\n  style,\n  onKeyDown,\n  ...props\n}: SluiceProps) {\n  const reducedMotion = useReducedMotion();\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const textareaRef = React.useRef<HTMLTextAreaElement>(null);\n  const fileInputRef = React.useRef<HTMLInputElement>(null);\n\n  // ---- Draft text (controlled / uncontrolled) ------------------------------\n  const textControlled = value !== undefined;\n  const [internalText, setInternalText] = React.useState(defaultValue);\n  const text = textControlled ? value : internalText;\n  const setText = React.useCallback(\n    (next: string) => {\n      if (!textControlled) setInternalText(next);\n      onValueChange?.(next);\n    },\n    [textControlled, onValueChange]\n  );\n\n  // ---- Attachments (controlled / uncontrolled) -----------------------------\n  const attachmentsControlled = attachments !== undefined;\n  const [internalAttachments, setInternalAttachments] = React.useState<\n    SluiceAttachment[]\n  >([]);\n  const currentAttachments = attachmentsControlled\n    ? attachments\n    : internalAttachments;\n  const idCounter = React.useRef(0);\n\n  const attach = React.useCallback(\n    (files: File[]) => {\n      if (files.length === 0) return;\n      onAttach?.(files);\n      if (!attachmentsControlled) {\n        setInternalAttachments((prev) => [\n          ...prev,\n          ...files.map((f) => ({\n            id: `sluice-file-${++idCounter.current}`,\n            name: f.name || \"pasted-file\",\n            size: f.size,\n          })),\n        ]);\n      }\n    },\n    [onAttach, attachmentsControlled]\n  );\n\n  const removeAttachment = React.useCallback(\n    (id: string) => {\n      onRemoveAttachment?.(id);\n      if (!attachmentsControlled) {\n        setInternalAttachments((prev) => prev.filter((a) => a.id !== id));\n      }\n      // The removed chip's button vanishes — keep focus in the composer.\n      textareaRef.current?.focus();\n    },\n    [onRemoveAttachment, attachmentsControlled]\n  );\n\n  // ---- Model (controlled / uncontrolled) -----------------------------------\n  const [internalModel, setInternalModel] = React.useState(\n    () => defaultModel ?? models?.[0] ?? \"\"\n  );\n  const currentModel = model ?? (internalModel || models?.[0] || \"\");\n  const changeModel = (next: string) => {\n    if (model === undefined) setInternalModel(next);\n    onModelChange?.(next);\n  };\n\n  // ---- Auto-grow -----------------------------------------------------------\n  const [overflowing, setOverflowing] = React.useState(false);\n  useIsoLayoutEffect(() => {\n    const ta = textareaRef.current;\n    if (!ta) return;\n    const cs = window.getComputedStyle(ta);\n    const lineHeight = parseFloat(cs.lineHeight) || 24;\n    const padY = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);\n    const max = lineHeight * Math.max(1, maxRows) + padY;\n    ta.style.height = \"0px\";\n    const needed = ta.scrollHeight;\n    ta.style.height = `${Math.min(needed, max)}px`;\n    setOverflowing(needed > max + 1);\n  }, [text, maxRows]);\n\n  // ---- Submit + lift-away ghost --------------------------------------------\n  const [ghost, setGhost] = React.useState<{\n    key: number;\n    text: string;\n    height: number;\n  } | null>(null);\n  const ghostCounter = React.useRef(0);\n\n  const isEmpty = text.trim().length === 0 && currentAttachments.length === 0;\n\n  const submit = () => {\n    if (busy || isEmpty) return;\n    const ta = textareaRef.current;\n    if (text.trim().length > 0) {\n      setGhost({\n        key: ++ghostCounter.current,\n        text,\n        height: ta?.offsetHeight ?? 0,\n      });\n    }\n    onSubmit?.(text, currentAttachments);\n    setText(\"\");\n    if (!attachmentsControlled) setInternalAttachments([]);\n  };\n\n  const handleTextKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n    if (event.key !== \"Enter\" || event.shiftKey) return;\n    // IME guard: Enter that confirms a composition must not submit.\n    if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n    event.preventDefault();\n    submit();\n  };\n\n  const handleRootKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(event);\n    if (event.defaultPrevented) return;\n    if (event.key === \"Escape\" && busy) {\n      event.preventDefault();\n      onStop?.();\n    }\n  };\n\n  const handlePaste = (event: React.ClipboardEvent<HTMLTextAreaElement>) => {\n    const files = Array.from(event.clipboardData.files);\n    if (files.length === 0 || hideAttach) return;\n    // Only swallow the paste when there's no text riding along with the files.\n    if (!event.clipboardData.getData(\"text/plain\")) event.preventDefault();\n    attach(files);\n  };\n\n  // ---- Pause the border trace offscreen ------------------------------------\n  const [onScreen, setOnScreen] = React.useState(true);\n  React.useEffect(() => {\n    const el = rootRef.current;\n    if (!el || typeof IntersectionObserver === \"undefined\") return;\n    const io = new IntersectionObserver(([entry]) =>\n      setOnScreen(entry?.isIntersecting ?? true)\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, []);\n\n  const showTrace = busy && !reducedMotion && onScreen;\n  const textClass = compact ? TEXT_CLASS_COMPACT : TEXT_CLASS;\n  const fade = { duration: 0.15 };\n  const morph = reducedMotion\n    ? { duration: 0 }\n    : ({ type: \"spring\", duration: 0.22, bounce: 0.25 } as const);\n\n  const iconButton = cn(\n    \"flex size-8 shrink-0 items-center justify-center rounded-full text-neutral-400 transition-colors\",\n    \"hover:bg-white/[0.06] hover:text-neutral-100\",\n    \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--sluice-accent)\"\n  );\n\n  return (\n    <div\n      ref={rootRef}\n      data-crucible=\"sluice\"\n      className={cn(\"relative w-full max-w-2xl\", className)}\n      style={{ [\"--sluice-accent\" as string]: accentColor, ...style }}\n      onKeyDown={handleRootKeyDown}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"relative border transition-[border-color,box-shadow] duration-300\",\n          compact\n            ? \"rounded-[14px] bg-white/[0.03]\"\n            : \"rounded-3xl bg-neutral-900/70 shadow-[inset_0_1px_0_rgba(255,255,255,0.06),0_18px_40px_-20px_rgba(0,0,0,0.9)] backdrop-blur-xl\",\n          busy && reducedMotion\n            ? \"border-[color-mix(in_oklab,var(--sluice-accent)_60%,transparent)]\"\n            : \"border-white/10 focus-within:border-white/20\",\n          busy &&\n            (compact\n              ? \"shadow-[0_0_24px_-12px_var(--sluice-accent)]\"\n              : \"shadow-[inset_0_1px_0_rgba(255,255,255,0.06),0_0_32px_-12px_var(--sluice-accent),0_18px_40px_-20px_rgba(0,0,0,0.9)]\")\n        )}\n      >\n        {/* Busy border trace — a rotating conic light masked to a 1.5px ring. */}\n        <AnimatePresence>\n          {showTrace && (\n            <motion.div\n              key=\"trace\"\n              aria-hidden\n              className=\"pointer-events-none absolute -inset-px overflow-hidden rounded-[inherit]\"\n              style={{\n                padding: 1.5,\n                WebkitMask:\n                  \"linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)\",\n                WebkitMaskComposite: \"xor\",\n                mask: \"linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)\",\n              }}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.3 }}\n            >\n              <motion.div\n                className=\"absolute aspect-square\"\n                style={{\n                  left: \"50%\",\n                  top: \"50%\",\n                  width: \"max(150%, 400px)\",\n                  x: \"-50%\",\n                  y: \"-50%\",\n                  background:\n                    \"conic-gradient(from 0deg, transparent 0deg, transparent 220deg, color-mix(in oklab, var(--sluice-accent) 40%, transparent) 290deg, var(--sluice-accent) 340deg, white 356deg, transparent 360deg)\",\n                }}\n                animate={{ rotate: 360 }}\n                transition={{ duration: 2.4, ease: \"linear\", repeat: Infinity }}\n              />\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        {/* Text area + the lift-away ghost of the last submission. */}\n        <div className=\"relative\">\n          <AnimatePresence>\n            {ghost && (\n              <motion.div\n                key={ghost.key}\n                aria-hidden\n                className=\"pointer-events-none absolute inset-x-0 bottom-0 flex flex-col justify-end overflow-hidden text-neutral-100\"\n                style={{ maxHeight: ghost.height || undefined }}\n                initial={{ opacity: 1, y: 0 }}\n                animate={\n                  reducedMotion ? { opacity: 0 } : { opacity: 0, y: -8 }\n                }\n                transition={\n                  reducedMotion ? fade : { duration: 0.34, ease: [0.22, 1, 0.36, 1] }\n                }\n                onAnimationComplete={() =>\n                  setGhost((g) => (g?.key === ghost.key ? null : g))\n                }\n              >\n                <div className={textClass}>{ghost.text}</div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n          <textarea\n            ref={textareaRef}\n            rows={1}\n            value={text}\n            placeholder={placeholder}\n            aria-label={label}\n            onChange={(e) => setText(e.target.value)}\n            onKeyDown={handleTextKeyDown}\n            onPaste={handlePaste}\n            className={cn(\n              textClass,\n              \"block resize-none bg-transparent text-neutral-100 placeholder:text-neutral-500 focus:outline-none\",\n              overflowing ? \"overflow-y-auto\" : \"overflow-y-hidden\"\n            )}\n          />\n        </div>\n\n        {/* Toolbar */}\n        <div\n          className={cn(\n            \"flex items-end gap-2\",\n            compact ? \"px-2 pb-2 pt-0.5\" : \"px-2.5 pb-2.5 pt-1\"\n          )}\n        >\n          {!hideAttach && (\n            <>\n              <button\n                type=\"button\"\n                aria-label=\"Attach files\"\n                className={iconButton}\n                onClick={() => fileInputRef.current?.click()}\n              >\n                <svg\n                  aria-hidden\n                  viewBox=\"0 0 24 24\"\n                  className=\"size-[18px]\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"1.8\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                >\n                  <path d=\"m20 11.5-8.2 8.2a5 5 0 0 1-7.1-7.1l8.6-8.6a3.4 3.4 0 0 1 4.8 4.8l-8.6 8.6a1.7 1.7 0 0 1-2.4-2.4l7.9-7.9\" />\n                </svg>\n              </button>\n              <input\n                ref={fileInputRef}\n                type=\"file\"\n                multiple\n                tabIndex={-1}\n                aria-hidden\n                className=\"hidden\"\n                onChange={(e) => {\n                  attach(Array.from(e.target.files ?? []));\n                  e.target.value = \"\";\n                }}\n              />\n            </>\n          )}\n\n          <ul\n            role=\"list\"\n            aria-label=\"Attachments\"\n            className=\"flex min-h-8 min-w-0 flex-1 flex-wrap items-center gap-y-1.5\"\n          >\n            <AnimatePresence initial={false}>\n              {currentAttachments.map((a) => (\n                <motion.li\n                  key={a.id}\n                  className=\"overflow-hidden\"\n                  style={{ originX: 0 }}\n                  initial={\n                    reducedMotion\n                      ? { opacity: 0 }\n                      : { opacity: 0, width: 0, marginRight: 0, x: -14, scale: 0.7 }\n                  }\n                  animate={{ opacity: 1, width: \"auto\", marginRight: 6, x: 0, scale: 1 }}\n                  exit={\n                    reducedMotion\n                      ? { opacity: 0, transition: fade }\n                      : {\n                          opacity: 0,\n                          width: 0,\n                          marginRight: 0,\n                          scale: 0.85,\n                          transition: { duration: 0.2, ease: [0.4, 0, 0.2, 1] },\n                        }\n                  }\n                  transition={\n                    reducedMotion\n                      ? fade\n                      : { type: \"spring\", stiffness: 520, damping: 30, mass: 0.7 }\n                  }\n                >\n                  <span className=\"flex h-7 items-center gap-1.5 whitespace-nowrap rounded-full border border-white/10 bg-white/[0.04] pl-2.5 pr-1 text-xs text-neutral-300\">\n                    <svg\n                      aria-hidden\n                      viewBox=\"0 0 24 24\"\n                      className=\"size-3.5 text-neutral-500\"\n                      fill=\"none\"\n                      stroke=\"currentColor\"\n                      strokeWidth=\"1.8\"\n                      strokeLinejoin=\"round\"\n                    >\n                      <path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z\" />\n                      <path d=\"M14 3v5h5\" />\n                    </svg>\n                    <span className=\"max-w-[10rem] truncate\">{a.name}</span>\n                    {a.size !== undefined && (\n                      <span className=\"tabular-nums text-neutral-500\">\n                        {formatSize(a.size)}\n                      </span>\n                    )}\n                    <button\n                      type=\"button\"\n                      aria-label={`Remove ${a.name}`}\n                      onClick={() => removeAttachment(a.id)}\n                      className=\"flex size-5 items-center justify-center rounded-full text-neutral-500 transition-colors hover:bg-white/10 hover:text-neutral-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--sluice-accent)\"\n                    >\n                      <svg\n                        aria-hidden\n                        viewBox=\"0 0 24 24\"\n                        className=\"size-3\"\n                        fill=\"none\"\n                        stroke=\"currentColor\"\n                        strokeWidth=\"2.2\"\n                        strokeLinecap=\"round\"\n                      >\n                        <path d=\"M6 6l12 12M18 6 6 18\" />\n                      </svg>\n                    </button>\n                  </span>\n                </motion.li>\n              ))}\n            </AnimatePresence>\n          </ul>\n\n          {models && models.length > 0 && (\n            <div className=\"relative shrink-0\">\n              <select\n                aria-label=\"Model\"\n                value={currentModel}\n                onChange={(e) => changeModel(e.target.value)}\n                className=\"h-8 cursor-pointer appearance-none rounded-full border border-white/10 bg-white/[0.03] pl-3 pr-7 text-xs text-neutral-300 transition-colors hover:border-white/20 hover:text-neutral-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--sluice-accent)\"\n              >\n                {models.map((m) => (\n                  <option key={m} value={m} className=\"bg-neutral-900 text-neutral-200\">\n                    {m}\n                  </option>\n                ))}\n              </select>\n              <svg\n                aria-hidden\n                viewBox=\"0 0 24 24\"\n                className=\"pointer-events-none absolute right-2.5 top-1/2 size-3 -translate-y-1/2 text-neutral-500\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2.2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\n                <path d=\"m6 9 6 6 6-6\" />\n              </svg>\n            </div>\n          )}\n\n          {/* Send ⇄ stop — one button that morphs disc → rounded square. */}\n          <motion.button\n            type=\"button\"\n            aria-label={busy ? \"Stop generating\" : \"Send message\"}\n            disabled={!busy && isEmpty}\n            onClick={() => (busy ? onStop?.() : submit())}\n            initial={false}\n            animate={{ borderRadius: busy ? 10 : 18 }}\n            transition={morph}\n            className={cn(\n              \"relative flex size-9 shrink-0 items-center justify-center overflow-hidden transition-colors duration-200\",\n              \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--sluice-accent) focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-900\",\n              busy\n                ? \"bg-[color-mix(in_oklab,var(--sluice-accent)_18%,transparent)] text-(--sluice-accent) hover:bg-[color-mix(in_oklab,var(--sluice-accent)_28%,transparent)]\"\n                : isEmpty\n                  ? \"cursor-not-allowed bg-white/[0.07] text-neutral-500\"\n                  : \"bg-neutral-100 text-neutral-950 hover:bg-white\"\n            )}\n          >\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              {busy ? (\n                <motion.span\n                  key=\"stop\"\n                  aria-hidden\n                  className=\"block size-3 rounded-[3px] bg-current\"\n                  initial={reducedMotion ? { opacity: 0 } : { opacity: 0, scale: 0.4, rotate: -45 }}\n                  animate={{ opacity: 1, scale: 1, rotate: 0 }}\n                  exit={reducedMotion ? { opacity: 0 } : { opacity: 0, scale: 0.4 }}\n                  transition={reducedMotion ? { duration: 0 } : morph}\n                />\n              ) : (\n                <motion.svg\n                  key=\"send\"\n                  aria-hidden\n                  viewBox=\"0 0 24 24\"\n                  className=\"size-[18px]\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"2.2\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  initial={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 8 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: -8 }}\n                  transition={reducedMotion ? { duration: 0 } : morph}\n                >\n                  <path d=\"M12 19V5M5.5 11.5 12 5l6.5 6.5\" />\n                </motion.svg>\n              )}\n            </AnimatePresence>\n          </motion.button>\n        </div>\n      </div>\n\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {busy ? \"Assistant is responding. Press Escape to stop.\" : \"\"}\n      </span>\n    </div>\n  );\n}\n"
    }
  ]
}