{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "selection-actions",
  "title": "Selection actions",
  "description": "Async rewrite actions for selected text with custom instructions, retry, keep, and discard controls.",
  "registryDependencies": [
    "https://beadkit.dev/r/ai-core.json"
  ],
  "files": [
    {
      "path": "components/ai/SelectionActions.tsx",
      "content": "\"use client\";\n\nimport { useState, type FormEvent } from \"react\";\nimport styles from \"./ai-state.module.css\";\n\nexport interface SelectionAction {\n  id: string;\n  label: string;\n  /** Optional accessible hint shown as the button description. */\n  description?: string;\n}\n\nexport interface SelectionActionsProps {\n  selectedText: string;\n  actions: readonly SelectionAction[];\n  onAction: (action: SelectionAction, selectedText: string, prompt?: string) => void | Promise<string | void>;\n  onAccept?: (result: string, action: SelectionAction) => void | Promise<void>;\n  onDiscard?: (result: string, action: SelectionAction) => void | Promise<void>;\n  initialResult?: string;\n  initialAction?: SelectionAction;\n  customAction?: SelectionAction;\n  errorMessage?: string;\n  className?: string;\n}\n\ntype ActionState = \"idle\" | \"working\" | \"result\" | \"error\";\n\nfunction ActionIcon({ id }: { id: string }) {\n  const normalized = id.toLowerCase();\n  if (normalized.includes(\"explain\")) {\n    return <svg aria-hidden=\"true\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\"><path d=\"M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v7a2.5 2.5 0 0 1-2.5 2.5H12l-4.5 5v-5h-1A2.5 2.5 0 0 1 4 12.5z\" /><path d=\"M8 8h8M8 11h5\" /></svg>;\n  }\n  if (normalized.includes(\"short\") || normalized.includes(\"trim\")) {\n    return <svg aria-hidden=\"true\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\"><circle cx=\"6\" cy=\"6\" r=\"2.5\" /><circle cx=\"18\" cy=\"18\" r=\"2.5\" /><path d=\"m8 8 8 8M16 8 8 16\" /></svg>;\n  }\n  if (normalized.includes(\"tone\")) {\n    return <svg aria-hidden=\"true\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\" /><path d=\"M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01\" /></svg>;\n  }\n  return <svg aria-hidden=\"true\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M12 3 13.8 9.2 20 11l-6.2 1.8L12 19l-1.8-6.2L4 11l6.2-1.8z\" /></svg>;\n}\n\nexport function SelectionActions({ selectedText, actions, onAction, onAccept, onDiscard, initialResult, initialAction, customAction = { id: \"custom\", label: \"Custom edit\" }, errorMessage = \"That edit could not be completed. Try again.\", className = \"\" }: SelectionActionsProps) {\n  const [state, setState] = useState<ActionState>(initialResult ? \"result\" : \"idle\");\n  const [activeAction, setActiveAction] = useState<SelectionAction | null>(initialAction ?? actions[0] ?? customAction);\n  const [result, setResult] = useState(initialResult ?? \"\");\n  const [prompt, setPrompt] = useState(\"\");\n  const [lastPrompt, setLastPrompt] = useState<string | undefined>();\n\n  async function run(action: SelectionAction, customPrompt?: string) {\n    if (state === \"working\") return;\n    setActiveAction(action);\n    setLastPrompt(customPrompt?.trim() || undefined);\n    setState(\"working\");\n    try {\n      const next = await onAction(action, selectedText, customPrompt?.trim() || undefined);\n      if (typeof next === \"string\") setResult(next);\n      setState(\"result\");\n    } catch {\n      setState(\"error\");\n    }\n  }\n\n  function submit(event: FormEvent<HTMLFormElement>) {\n    event.preventDefault();\n    const custom = prompt.trim();\n    if (!custom) return;\n    void run(customAction, custom);\n  }\n\n  function reset() {\n    setState(\"idle\");\n    setActiveAction(null);\n    setResult(\"\");\n    setPrompt(\"\");\n    setLastPrompt(undefined);\n  }\n\n  const busy = state === \"working\";\n  return (\n    <div data-ai-state=\"selection-actions\" className={`w-full max-w-md ${className}`}>\n      <p className=\"text-muted-foreground mb-2 line-clamp-3 text-sm leading-relaxed\">{selectedText}</p>\n      <div className={`${styles.fadeIn} bg-card flex w-full flex-col gap-2 rounded-xl border p-2 shadow-sm`}>\n        {state === \"working\" ? (\n          <div className=\"text-muted-foreground flex min-h-9 items-center gap-2 px-2 text-xs\" role=\"status\" aria-live=\"polite\">\n            <span aria-hidden=\"true\" className=\"border-muted-foreground/30 border-t-foreground size-3 shrink-0 rounded-full border-[1.5px] motion-safe:animate-spin\" />\n            {activeAction?.label ?? \"Working\"}…\n          </div>\n        ) : state === \"result\" ? (\n          <div className=\"flex flex-col gap-2\">\n            <p className=\"text-foreground whitespace-pre-wrap px-2 text-sm leading-relaxed\" aria-live=\"polite\">{result || \"Done.\"}</p>\n            <div className=\"flex flex-wrap items-center gap-2 border-t pt-2\">\n              <button type=\"button\" disabled={!activeAction} onClick={() => { if (activeAction) void onAccept?.(result, activeAction); reset(); }} className=\"bg-primary text-primary-foreground inline-flex min-h-8 items-center gap-1.5 rounded-full px-3 text-xs font-medium transition-[opacity,transform] duration-150 hover:opacity-90 active:scale-[0.97]\">✓ Keep</button>\n              <button type=\"button\" onClick={() => { if (activeAction) void onDiscard?.(result, activeAction); reset(); }} className=\"text-muted-foreground hover:bg-muted hover:text-foreground inline-flex min-h-8 items-center rounded-full px-3 text-xs transition-colors duration-150\">Discard</button>\n              <button type=\"button\" onClick={() => { if (activeAction) void run(activeAction, lastPrompt); }} className=\"text-muted-foreground hover:bg-muted hover:text-foreground ml-auto inline-flex min-h-8 items-center rounded-full px-3 text-xs transition-colors duration-150 motion-reduce:transition-none\">Try again</button>\n            </div>\n          </div>\n        ) : state === \"error\" ? (\n          <div className=\"flex min-h-9 flex-wrap items-center gap-2 px-2\">\n            <p className=\"text-destructive flex-1 text-xs\" role=\"alert\">{errorMessage}</p>\n            <button type=\"button\" onClick={() => { if (activeAction) void run(activeAction, lastPrompt); }} className=\"hover:bg-muted min-h-8 rounded-full px-3 text-xs font-medium\">Retry</button>\n            <button type=\"button\" onClick={reset} className=\"text-muted-foreground hover:text-foreground min-h-8 px-2 text-xs\">Cancel</button>\n          </div>\n        ) : (\n          <>\n            <div className=\"flex max-w-full flex-wrap items-center gap-1\">\n              {actions.map((action) => (\n                <button key={action.id} type=\"button\" title={action.description} aria-label={action.description ? `${action.label}: ${action.description}` : action.label} disabled={busy} onClick={() => void run(action)} className=\"text-foreground hover:bg-muted inline-flex min-h-8 items-center gap-1.5 rounded-full px-2.5 text-xs transition-[background-color,color,transform] duration-150 active:scale-[0.97] disabled:cursor-not-allowed disabled:opacity-50\">\n                  <ActionIcon id={action.id} />{action.label}\n                </button>\n              ))}\n            </div>\n            <form onSubmit={submit} className=\"border-border flex min-w-0 items-center gap-1 border-t pt-2\">\n              <input value={prompt} onChange={(event) => setPrompt(event.target.value)} disabled={busy} aria-label=\"Describe an edit\" placeholder=\"Describe an edit\" className=\"text-foreground placeholder:text-muted-foreground min-h-8 min-w-0 flex-1 bg-transparent px-2 text-xs outline-none\" />\n              <button type=\"submit\" disabled={!prompt.trim() || busy} aria-label=\"Send edit instruction\" className=\"bg-primary text-primary-foreground flex size-8 shrink-0 items-center justify-center rounded-full text-sm transition-[opacity,transform] duration-150 hover:opacity-90 active:scale-[0.94] disabled:opacity-40 motion-reduce:transition-none\">↑</button>\n            </form>\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai/SelectionActions.tsx"
    }
  ],
  "type": "registry:component"
}
