{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"selection-actions","type":"registry:component","title":"Selection actions","description":"Select text and a pill appears under it. Pick an action or describe an edit, and the rewrite streams in place.","author":"Hairline UI","dependencies":["lucide-react"],"files":[{"path":"registry/selection-actions.tsx","type":"registry:component","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { ArrowUp, Check, ChevronRight, RotateCcw, Square, X } from \"lucide-react\";\n\n/* ─────────────────────────────────────────────────────────\n * SELECTION ACTIONS: select text, rewrite it in place\n *\n *   open       a pill under the selection: a field for your own\n *              instruction, quick actions, more behind the chevron\n *   typing     the actions fold away and the field takes the room\n *   thinking   a spinner and \"Improving…\" until the first words\n *   streaming  the rewrite streams into the selection itself\n *   result     Keep, Discard or try again\n *   error      it didn't work; try again\n *\n * Give it the text as value. onAction gets the selection and\n * returns the rewrite: a string, a promise of one, or an async\n * iterable of chunks to stream. Keep calls onValueChange.\n * ───────────────────────────────────────────────────────── */\n\nexport interface SelectionAction {\n  id: string;\n  label: string;\n  icon?: React.ReactNode;\n  /** Shown while it runs, e.g. \"Shortening\". */\n  pendingLabel?: string;\n}\n\nexport interface SelectionRequest {\n  /** The action's id, or \"custom\" for a typed instruction. */\n  id: string;\n  /** The selected text. */\n  text: string;\n  instruction?: string;\n  /** The text around the selection, for context. */\n  before: string;\n  after: string;\n  /** Aborted when the person stops or discards. */\n  signal: AbortSignal;\n}\n\n/** The rewrite, whole or streamed. Return nothing for actions handled elsewhere (e.g. Explain opening a chat). */\nexport type SelectionResult = string | AsyncIterable<string> | Promise<string | void> | void;\n\nexport interface SelectionActionsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  value: string;\n  onValueChange: (value: string) => void;\n  /** Always visible. */\n  actions: SelectionAction[];\n  /** Behind the chevron. */\n  more?: SelectionAction[];\n  onAction: (request: SelectionRequest) => SelectionResult;\n  placeholder?: string;\n}\n\ntype Mode = \"idle\" | \"thinking\" | \"streaming\" | \"result\" | \"error\";\ntype Edit = { start: number; end: number; original: string; id: string; label: string; instruction?: string };\n\nconst EASE = \"cubic-bezier(0.23,1,0.32,1)\";\nconst GLIDE = \"cubic-bezier(0.77,0,0.175,1)\";\nconst FIELD = 148;\nconst FIELD_MIN = 72;\nconst HIGHLIGHT = \"ui-selection\";\nconst FOCUS = \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40\";\nconst QUIET = `inline-flex h-7 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 text-[12.5px] text-foreground transition-[background-color,transform] duration-150 hover:bg-accent active:scale-[0.96] [&_svg]:size-3.5 [&_svg]:text-muted-foreground ${FOCUS}`;\nconst ICON = `flex size-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-[background-color,color,transform] duration-150 hover:bg-accent hover:text-foreground active:scale-[0.96] [&_svg]:size-3.5 ${FOCUS}`;\nconst SHIMMER =\n  \"bg-[linear-gradient(90deg,color-mix(in_oklab,var(--muted-foreground)_55%,transparent)_35%,var(--foreground)_50%,color-mix(in_oklab,var(--muted-foreground)_55%,transparent)_65%)] bg-[length:200%_100%] bg-clip-text text-transparent animate-[ui-shimmer_1.4s_linear_infinite] motion-reduce:animate-none motion-reduce:bg-none motion-reduce:text-foreground/70\";\n\n// The Highlight API is newer than some TypeScript DOM libs; reach it through guarded casts.\ntype Registry = { set(name: string, highlight: unknown): void; delete(name: string): void };\nconst registry = () => (typeof CSS !== \"undefined\" ? (CSS as unknown as { highlights?: Registry }).highlights : undefined);\nconst HighlightClass = () => (globalThis as unknown as { Highlight?: new (...ranges: Range[]) => unknown }).Highlight;\n\nconst isAsyncIterable = (x: unknown): x is AsyncIterable<string> => typeof x === \"object\" && x !== null && Symbol.asyncIterator in x;\n\n/** Characters from the start of root to (node, offset). */\nfunction offsetIn(root: Node, node: Node, offset: number) {\n  const range = document.createRange();\n  range.setStart(root, 0);\n  range.setEnd(node, offset);\n  return range.toString().length;\n}\n\nconst reducedQuery = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReduced = (onChange: () => void) => {\n  const query = window.matchMedia(reducedQuery);\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n};\nconst useReducedMotion = () =>\n  React.useSyncExternalStore(subscribeReduced, () => window.matchMedia(reducedQuery).matches, () => false);\n\n/** A row that folds to zero width. Its natural width is measured so the fold animates exactly. */\nfunction Fold({\n  open,\n  width,\n  onWidth,\n  reduced,\n  gap = true,\n  children,\n}: {\n  open: boolean;\n  width?: number;\n  onWidth?: (width: number) => void;\n  reduced: boolean;\n  gap?: boolean;\n  children: React.ReactNode;\n}) {\n  const inner = React.useRef<HTMLDivElement>(null);\n  React.useLayoutEffect(() => {\n    const el = inner.current;\n    if (!el || !onWidth) return;\n    const report = () => onWidth(el.offsetWidth);\n    report();\n    const observer = new ResizeObserver(report);\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [onWidth]);\n\n  return (\n    <div\n      inert={!open}\n      className=\"flex shrink-0 items-center overflow-hidden\"\n      style={{\n        maxWidth: open ? width : 0,\n        opacity: open ? 1 : 0,\n        transform: open ? \"none\" : \"translateX(-6px)\",\n        transition: reduced ? \"none\" : `max-width 400ms ${EASE}, opacity 300ms ${EASE}, transform 400ms ${EASE}`,\n      }}\n    >\n      <div ref={inner} className={`flex w-max shrink-0 items-center gap-0.5 ${gap ? \"pl-0.5\" : \"\"}`}>\n        {children}\n      </div>\n    </div>\n  );\n}\n\nconst Divider = () => <span aria-hidden=\"true\" className=\"mx-1 h-4 w-px shrink-0 bg-border\" />;\n\nexport function SelectionActions({\n  value,\n  onValueChange,\n  actions,\n  more = [],\n  onAction,\n  placeholder = \"Describe edits\",\n  className = \"\",\n  ...props\n}: SelectionActionsProps) {\n  const reduced = useReducedMotion();\n  const [open, setOpen] = React.useState(false);\n  const [ready, setReady] = React.useState(false);\n  const [mode, setModeState] = React.useState<Mode>(\"idle\");\n  const [edit, setEdit] = React.useState<Edit | null>(null);\n  const [chunks, setChunks] = React.useState<string[]>([]);\n  const [anchor, setAnchor] = React.useState({ x: 0, y: 0 });\n  const [expanded, setExpanded] = React.useState(false);\n  const [prompt, setPrompt] = React.useState(\"\");\n  const [typingWidth, setTypingWidth] = React.useState<number | null>(null);\n  const [rootWidth, setRootWidth] = React.useState(0);\n  const [widths, setWidths] = React.useState<Record<string, number>>({});\n\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const textRef = React.useRef<HTMLDivElement>(null);\n  const markRef = React.useRef<HTMLSpanElement>(null);\n  const barRef = React.useRef<HTMLDivElement>(null);\n  const contentRef = React.useRef<HTMLDivElement>(null);\n  const keepRef = React.useRef<HTMLButtonElement>(null);\n  const rangeRef = React.useRef<Range | null>(null);\n  const selRef = React.useRef<{ start: number; end: number } | null>(null);\n  const editRef = React.useRef<Edit | null>(null);\n  const modeRef = React.useRef<Mode>(\"idle\");\n  const openRef = React.useRef(false);\n  const draggingRef = React.useRef(false);\n  const streamedRef = React.useRef(\"\");\n  const controllerRef = React.useRef<AbortController | null>(null);\n  const frameRef = React.useRef(0);\n  const valueRef = React.useRef(value);\n  const onActionRef = React.useRef(onAction);\n  React.useLayoutEffect(() => {\n    valueRef.current = value;\n    onActionRef.current = onAction;\n  });\n\n  const setMode = (next: Mode) => {\n    modeRef.current = next;\n    setModeState(next);\n  };\n\n  const report = React.useMemo(() => {\n    const make = (key: string) => (n: number) => setWidths((w) => (w[key] === n ? w : { ...w, [key]: n }));\n    return { primary: make(\"primary\"), more: make(\"more\"), tail: make(\"tail\"), send: make(\"send\") };\n  }, []);\n\n  const close = React.useCallback(() => {\n    controllerRef.current?.abort();\n    registry()?.delete(HIGHLIGHT);\n    rangeRef.current = null;\n    selRef.current = null;\n    editRef.current = null;\n    streamedRef.current = \"\";\n    openRef.current = false;\n    modeRef.current = \"idle\";\n    setOpen(false);\n    setModeState(\"idle\");\n    setEdit(null);\n    setChunks([]);\n    setExpanded(false);\n    setPrompt(\"\");\n    setTypingWidth(null);\n  }, []);\n\n  /* Centre on the whole selection, sit under its last line, stay inside the container. */\n  const measure = React.useCallback(() => {\n    const root = rootRef.current;\n    const target: Range | HTMLElement | null = markRef.current ?? rangeRef.current;\n    if (!root || !target) return null;\n    const rects = target.getClientRects();\n    const last = rects[rects.length - 1];\n    if (!last) return null;\n    const bounds = target.getBoundingClientRect();\n    const box = root.getBoundingClientRect();\n    const half = (barRef.current?.offsetWidth ?? 0) / 2;\n    const x = bounds.left - box.left + bounds.width / 2;\n    // A bar wider than the text is centred on it rather than pinned to one side.\n    const lo = Math.min(half, box.width / 2);\n    const hi = Math.max(box.width - half, box.width / 2);\n    return {\n      x: Math.round(Math.min(Math.max(x, lo), hi)),\n      y: Math.round(last.bottom - box.top + 8),\n    };\n  }, []);\n\n  const place = React.useCallback(() => {\n    cancelAnimationFrame(frameRef.current);\n    frameRef.current = requestAnimationFrame(() => {\n      const next = measure();\n      if (next) setAnchor((a) => (a.x === next.x && a.y === next.y ? a : next));\n    });\n  }, [measure]);\n\n  /* Read the selection inside the text. Ignored while an edit runs or the bar has focus. */\n  const evaluate = React.useCallback(() => {\n    if (editRef.current || barRef.current?.contains(document.activeElement)) return;\n    const text = textRef.current;\n    const node = text?.firstChild;\n    const selection = window.getSelection();\n    const range = selection && selection.rangeCount ? selection.getRangeAt(0) : null;\n    if (!text || !node || !range || range.collapsed || !text.contains(range.startContainer) || !text.contains(range.endContainer)) {\n      if (openRef.current) close();\n      return;\n    }\n    const v = valueRef.current;\n    let start = offsetIn(text, range.startContainer, range.startOffset);\n    let end = offsetIn(text, range.endContainer, range.endOffset);\n    while (start < end && /\\s/.test(v[start])) start++;\n    while (end > start && /\\s/.test(v[end - 1])) end--;\n    if (start >= end) {\n      if (openRef.current) close();\n      return;\n    }\n    const trimmed = document.createRange();\n    trimmed.setStart(node, start);\n    trimmed.setEnd(node, end);\n    rangeRef.current = trimmed;\n    selRef.current = { start, end };\n    const Highlight = HighlightClass();\n    if (Highlight) registry()?.set(HIGHLIGHT, new Highlight(trimmed));\n    const next = measure();\n    if (next) setAnchor(next);\n    openRef.current = true;\n    setOpen(true);\n  }, [close, measure]);\n\n  const execute = async (target: Edit) => {\n    controllerRef.current?.abort();\n    const controller = new AbortController();\n    controllerRef.current = controller;\n    const v = valueRef.current;\n    let out: SelectionResult;\n    try {\n      out = onActionRef.current({\n        id: target.id,\n        text: target.original,\n        instruction: target.instruction,\n        before: v.slice(0, target.start),\n        after: v.slice(target.end),\n        signal: controller.signal,\n      });\n    } catch (error) {\n      out = Promise.reject(error);\n    }\n    if (out === undefined) {\n      close();\n      return;\n    }\n\n    window.getSelection()?.removeAllRanges();\n    registry()?.delete(HIGHLIGHT);\n    editRef.current = target;\n    streamedRef.current = \"\";\n    setEdit(target);\n    setChunks([]);\n    setExpanded(false);\n    setPrompt(\"\");\n    setTypingWidth(null);\n    setMode(\"thinking\");\n\n    try {\n      if (isAsyncIterable(out)) {\n        for await (const chunk of out) {\n          if (controller.signal.aborted) return;\n          if (!chunk) continue;\n          streamedRef.current += chunk;\n          setChunks((c) => [...c, chunk]);\n          setMode(\"streaming\");\n        }\n      } else {\n        const whole = await out;\n        if (controller.signal.aborted) return;\n        if (typeof whole !== \"string\") {\n          close();\n          return;\n        }\n        streamedRef.current = whole;\n        setChunks(whole ? [whole] : []);\n      }\n      if (controller.signal.aborted) return;\n      setMode(streamedRef.current.trim() ? \"result\" : \"error\");\n    } catch {\n      if (!controller.signal.aborted) setMode(\"error\");\n    }\n  };\n\n  const start = (id: string, label: string, instruction?: string) => {\n    const sel = selRef.current;\n    if (!sel) return;\n    execute({ ...sel, original: valueRef.current.slice(sel.start, sel.end), id, label, instruction });\n  };\n\n  const keep = () => {\n    const target = editRef.current;\n    if (!target) return;\n    const v = valueRef.current;\n    onValueChange(v.slice(0, target.start) + streamedRef.current + v.slice(target.end));\n    close();\n  };\n\n  /* Stopping keeps what has streamed so far for review; with nothing yet, it's a discard. */\n  const stop = () => {\n    controllerRef.current?.abort();\n    if (streamedRef.current.trim()) setMode(\"result\");\n    else close();\n  };\n\n  const handlers = React.useRef({ close, stop });\n  React.useLayoutEffect(() => {\n    handlers.current = { close, stop };\n  });\n\n  // Follow the selection: after a drag ends, and as keyboard or touch handles adjust it.\n  React.useEffect(() => {\n    const onSelection = () => {\n      if (!draggingRef.current) evaluate();\n    };\n    const onUp = () => {\n      if (!draggingRef.current) return;\n      draggingRef.current = false;\n      evaluate();\n    };\n    document.addEventListener(\"selectionchange\", onSelection);\n    document.addEventListener(\"pointerup\", onUp);\n    return () => {\n      document.removeEventListener(\"selectionchange\", onSelection);\n      document.removeEventListener(\"pointerup\", onUp);\n      controllerRef.current?.abort();\n      registry()?.delete(HIGHLIGHT);\n    };\n  }, [evaluate]);\n\n  // Escape backs out one step; a click outside closes an untouched selection.\n  React.useEffect(() => {\n    if (!open) return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key !== \"Escape\") return;\n      const m = modeRef.current;\n      if (m === \"thinking\" || m === \"streaming\") handlers.current.stop();\n      else handlers.current.close();\n    };\n    const onDown = (e: PointerEvent) => {\n      if (!editRef.current && !rootRef.current?.contains(e.target as Node)) handlers.current.close();\n    };\n    document.addEventListener(\"keydown\", onKey);\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => {\n      document.removeEventListener(\"keydown\", onKey);\n      document.removeEventListener(\"pointerdown\", onDown);\n    };\n  }, [open]);\n\n  // Turn on the glide only after the bar has painted in place, so it never flies in from a corner.\n  React.useEffect(() => {\n    if (!open) {\n      setReady(false);\n      return;\n    }\n    let inner = 0;\n    const outer = requestAnimationFrame(() => {\n      inner = requestAnimationFrame(() => setReady(true));\n    });\n    return () => {\n      cancelAnimationFrame(outer);\n      cancelAnimationFrame(inner);\n    };\n  }, [open]);\n\n  React.useLayoutEffect(() => {\n    if (open) place();\n  }, [open, mode, chunks, expanded, typingWidth, place]);\n\n  React.useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    const observer = new ResizeObserver(() => {\n      setRootWidth(root.clientWidth);\n      if (openRef.current) place();\n    });\n    observer.observe(root);\n    return () => {\n      observer.disconnect();\n      cancelAnimationFrame(frameRef.current);\n    };\n  }, [place]);\n\n  /* When the whole content changes (actions, progress, Keep), morph the bar from its last width. */\n  const lastWidth = React.useRef(0);\n  const previousMode = React.useRef<Mode>(\"idle\");\n  const widthAnimation = React.useRef<Animation | null>(null);\n  React.useLayoutEffect(() => {\n    const bar = barRef.current;\n    const content = contentRef.current;\n    if (!bar || !content) {\n      lastWidth.current = 0;\n      previousMode.current = mode;\n      return;\n    }\n    const next = Math.ceil(content.getBoundingClientRect().width) + 8;\n    const previous = lastWidth.current;\n    // The new content rises out of a light blur as the bar takes its width (progress → Keep, actions → progress…).\n    const busyBoth = (m: Mode) => m === \"thinking\" || m === \"streaming\";\n    if (previousMode.current !== mode && previous && !reduced && !(busyBoth(previousMode.current) && busyBoth(mode))) {\n      content.animate(\n        [\n          { opacity: 0, filter: \"blur(4px)\" },\n          { opacity: 1, filter: \"blur(0px)\" },\n        ],\n        { duration: 300, easing: \"cubic-bezier(0.16,1,0.3,1)\", delay: 60, fill: \"backwards\" }\n      );\n    }\n    if (previousMode.current !== mode && previous && Math.abs(next - previous) > 1 && !reduced) {\n      widthAnimation.current?.cancel();\n      const animation = bar.animate([{ width: `${previous}px` }, { width: `${next}px` }], { duration: 320, easing: EASE });\n      widthAnimation.current = animation;\n      animation.onfinish = () => {\n        if (widthAnimation.current === animation) widthAnimation.current = null;\n      };\n    }\n    lastWidth.current = next;\n    previousMode.current = mode;\n  }, [mode, open, reduced]);\n\n  React.useEffect(() => {\n    const content = contentRef.current;\n    if (!open || !content) return;\n    const observer = new ResizeObserver(() => {\n      place(); // re-centre as folds open and close\n      if (widthAnimation.current?.playState === \"running\") return;\n      lastWidth.current = Math.ceil(content.getBoundingClientRect().width) + 8;\n    });\n    observer.observe(content);\n    return () => {\n      observer.disconnect();\n      widthAnimation.current?.cancel();\n    };\n  }, [open, place]);\n\n  // Hand focus to Keep when the rewrite is ready, unless the person has moved on.\n  React.useEffect(() => {\n    if (mode !== \"result\") return;\n    const active = document.activeElement;\n    if (!active || active === document.body || barRef.current?.contains(active)) keepRef.current?.focus({ preventScroll: true });\n  }, [mode]);\n\n  // Arrow keys move between the buttons.\n  const onToolbarKey = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    if (e.key !== \"ArrowRight\" && e.key !== \"ArrowLeft\") return;\n    const buttons = Array.from(barRef.current?.querySelectorAll<HTMLButtonElement>(\"button:not([disabled])\") ?? []).filter(\n      (b) => !b.closest(\"[inert]\")\n    );\n    const i = buttons.indexOf(document.activeElement as HTMLButtonElement);\n    if (i < 0) return;\n    e.preventDefault();\n    buttons[(i + (e.key === \"ArrowRight\" ? 1 : -1) + buttons.length) % buttons.length]?.focus();\n  };\n\n  const typing = prompt.trim().length > 0 && typingWidth !== null;\n  const send = widths.send ?? 30;\n  const restWidth = rootWidth\n    ? Math.max(FIELD_MIN, Math.min(FIELD, rootWidth - 8 - 11 - (widths.primary ?? 0) - (more.length ? widths.tail ?? 0 : 0)))\n    : FIELD;\n  const fieldWidth = typing && typingWidth ? typingWidth - 8 - send : restWidth;\n  const busy = mode === \"thinking\" || mode === \"streaming\";\n  const label = edit?.label ?? \"Editing\";\n\n  const action = (item: SelectionAction) => (\n    <button key={item.id} type=\"button\" onClick={() => start(item.id, item.pendingLabel ?? item.label)} className={QUIET}>\n      {item.icon}\n      {item.label}\n    </button>\n  );\n\n  return (\n    <div ref={rootRef} className={`relative ${className}`} {...props}>\n      <div\n        ref={textRef}\n        onPointerDown={() => {\n          if (editRef.current) return;\n          draggingRef.current = true;\n          if (openRef.current) close();\n        }}\n        className=\"whitespace-pre-wrap\"\n      >\n        {edit ? (\n          <>\n            {value.slice(0, edit.start)}\n            <span ref={markRef} className=\"rounded-[3px] bg-primary/15 box-decoration-clone\">\n              {mode === \"streaming\" || mode === \"result\" ? (\n                chunks.map((chunk, i) => (\n                  <span key={i} className=\"animate-[ui-fade-in_280ms_ease-out_both] motion-reduce:animate-none\">\n                    {chunk}\n                  </span>\n                ))\n              ) : (\n                <span className={`transition-opacity duration-300 ${mode === \"thinking\" ? \"opacity-55\" : \"\"}`}>{edit.original}</span>\n              )}\n            </span>\n            {value.slice(edit.end)}\n          </>\n        ) : (\n          value\n        )}\n      </div>\n\n      {open && (\n        <div\n          className=\"absolute left-0 top-0 z-50\"\n          style={{\n            transform: `translate3d(${anchor.x}px, ${anchor.y}px, 0) translateX(-50%)`,\n            transition: ready && !reduced ? `transform 320ms ${GLIDE}` : \"none\",\n          }}\n        >\n          <div\n            ref={barRef}\n            role=\"toolbar\"\n            aria-label=\"Edit selection\"\n            onKeyDown={onToolbarKey}\n            // Keep the text selected while clicking the bar (the field still takes focus).\n            onPointerDown={(e) => {\n              if (!(e.target instanceof HTMLInputElement)) e.preventDefault();\n            }}\n            className=\"flex h-9 w-fit max-w-[calc(100vw-32px)] select-none items-center justify-center overflow-hidden rounded-full bg-background p-1 text-foreground shadow-[0_0_0_1px_var(--border)] animate-[ui-pop-in_220ms_cubic-bezier(0.23,1,0.32,1)_both] motion-reduce:animate-none\"\n            style={{ width: mode === \"idle\" && typing ? typingWidth ?? undefined : undefined }}\n          >\n            <div\n              ref={contentRef}\n              className=\"flex w-max shrink-0 items-center\"\n              style={{ width: mode === \"idle\" && typing && typingWidth ? typingWidth - 8 : undefined }}\n            >\n              {mode === \"idle\" && (\n                <>\n                  <Fold open={!expanded} width={fieldWidth} reduced={reduced} gap={false}>\n                    <form\n                      onSubmit={(e) => {\n                        e.preventDefault();\n                        if (prompt.trim()) start(\"custom\", \"Editing\", prompt.trim());\n                      }}\n                      className=\"flex h-7 items-center\"\n                      style={{ width: fieldWidth, transition: reduced ? \"none\" : `width 400ms ${EASE}` }}\n                    >\n                      <input\n                        value={prompt}\n                        onChange={(e) => {\n                          const next = e.target.value;\n                          if (!prompt.trim() && next.trim()) {\n                            const now = Math.ceil(barRef.current?.getBoundingClientRect().width ?? 0);\n                            setTypingWidth(Math.max(now, 8 + FIELD + send));\n                          } else if (!next.trim()) setTypingWidth(null);\n                          setPrompt(next);\n                        }}\n                        placeholder={placeholder}\n                        aria-label={placeholder}\n                        className=\"h-7 w-full min-w-0 text-ellipsis bg-transparent pl-3 pr-2 text-[12.5px] text-foreground outline-none placeholder:text-muted-foreground\"\n                      />\n                    </form>\n                  </Fold>\n                  <Fold open={!typing && !expanded} width={9} reduced={reduced} gap={false}>\n                    <Divider />\n                  </Fold>\n                  <Fold open={!typing} width={widths.primary} onWidth={report.primary} reduced={reduced} gap={false}>\n                    {actions.map(action)}\n                  </Fold>\n                  {more.length > 0 && (\n                    <>\n                      <Fold open={!typing && expanded} width={widths.more} onWidth={report.more} reduced={reduced}>\n                        {more.map(action)}\n                      </Fold>\n                      <Fold open={!typing} width={widths.tail} onWidth={report.tail} reduced={reduced}>\n                        <Divider />\n                        <button\n                          type=\"button\"\n                          aria-label={expanded ? \"Fewer actions\" : \"More actions\"}\n                          aria-expanded={expanded}\n                          onClick={() => setExpanded((x) => !x)}\n                          className={`${ICON} text-foreground`}\n                        >\n                          <ChevronRight\n                            className=\"transition-transform duration-[400ms]\"\n                            style={{ transform: expanded ? \"rotate(180deg)\" : \"none\", transitionTimingFunction: EASE }}\n                          />\n                        </button>\n                      </Fold>\n                    </>\n                  )}\n                  <Fold open={typing} width={send} onWidth={report.send} reduced={reduced}>\n                    <button\n                      type=\"button\"\n                      aria-label=\"Send edit\"\n                      onClick={() => prompt.trim() && start(\"custom\", \"Editing\", prompt.trim())}\n                      className={`flex size-7 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground transition-transform duration-150 active:scale-[0.94] ${FOCUS}`}\n                    >\n                      <ArrowUp className=\"size-4\" strokeWidth={2.4} />\n                    </button>\n                  </Fold>\n                </>\n              )}\n\n              {busy && (\n                <>\n                  <span role=\"status\" className=\"inline-flex h-7 items-center gap-1.5 whitespace-nowrap pl-2.5 pr-1.5 text-[12.5px] text-muted-foreground\">\n                    <span\n                      aria-hidden=\"true\"\n                      className=\"size-3 shrink-0 animate-spin rounded-full border-[1.5px] border-border border-t-muted-foreground motion-reduce:animate-none\"\n                    />\n                    {mode === \"thinking\" ? <span className={SHIMMER}>{label}…</span> : <span>{label}…</span>}\n                  </span>\n                  <Divider />\n                  <button type=\"button\" aria-label=\"Stop\" onClick={stop} className={ICON}>\n                    <Square className=\"!size-2.5\" fill=\"currentColor\" />\n                  </button>\n                </>\n              )}\n\n              {mode === \"result\" && (\n                <>\n                  <span className=\"sr-only\" role=\"status\">\n                    Rewrite ready. Keep or discard it.\n                  </span>\n                  <button\n                    ref={keepRef}\n                    type=\"button\"\n                    onClick={keep}\n                    className={`inline-flex h-7 shrink-0 items-center gap-1 rounded-full bg-primary px-2.5 text-[12.5px] text-primary-foreground transition-[opacity,transform] duration-150 hover:opacity-90 active:scale-[0.96] [&_svg]:size-3.5 ${FOCUS}`}\n                  >\n                    <Check />\n                    Keep\n                  </button>\n                  <button type=\"button\" onClick={close} className={`${QUIET} ml-0.5`}>\n                    <X />\n                    Discard\n                  </button>\n                  <Divider />\n                  <button type=\"button\" aria-label=\"Try again\" onClick={() => editRef.current && execute(editRef.current)} className={ICON}>\n                    <RotateCcw />\n                  </button>\n                </>\n              )}\n\n              {mode === \"error\" && (\n                <>\n                  <span role=\"alert\" className=\"whitespace-nowrap pl-2.5 pr-1 text-[12.5px] text-muted-foreground\">\n                    That didn’t work\n                  </span>\n                  <button type=\"button\" onClick={() => editRef.current && execute(editRef.current)} className={QUIET}>\n                    <RotateCcw />\n                    Try again\n                  </button>\n                  <Divider />\n                  <button type=\"button\" aria-label=\"Discard\" onClick={close} className={ICON}>\n                    <X />\n                  </button>\n                </>\n              )}\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"}],"css":{"@keyframes ui-shimmer":{"0%":{"background-position":"150% 0"},"100%":{"background-position":"-50% 0"}},"@keyframes ui-fade-in":{"from":{"opacity":"0"},"to":{"opacity":"1"}},"@keyframes ui-pop-in":{"from":{"opacity":"0","transform":"translateY(-4px) scale(0.96)"},"to":{"opacity":"1","transform":"none"}},"@layer base":{"::highlight(ui-selection)":{"background-color":"color-mix(in oklab, var(--primary) 18%, transparent)"}}},"docs":"Docs and live preview: https://hairlineui.com/ui/selection-actions"}