{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "approval-prompt",
  "type": "registry:ui",
  "title": "Approval Prompt",
  "description": "Human-in-the-loop permission card (“Kura wants to run `rm -rf build`”) with risk level, details, Approve/Deny/Always allow, ⌘↵ and Esc, and resolved states.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/button.json",
    "https://kuro.abstergo.fyi/r/kbd.json",
    "https://kuro.abstergo.fyi/r/use-controllable-state.json",
    "https://kuro.abstergo.fyi/r/use-hotkey.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/approval-prompt/approval-prompt.tsx",
      "type": "registry:ui",
      "target": "components/kura/approval-prompt/approval-prompt.tsx",
      "content": "\"use client\";\n\nimport { Check, ChevronRight, ShieldCheck, Terminal, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useControllableState } from \"@/hooks/kura/use-controllable-state\";\nimport { useHotkey } from \"@/hooks/kura/use-hotkey\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/kura/button\";\nimport { Kbd } from \"@/components/kura/kbd\";\n\nexport type ApprovalDecision = \"pending\" | \"approved\" | \"denied\" | \"always\";\n\nexport interface ApprovalPromptProps extends Omit<React.ComponentProps<\"section\">, \"title\"> {\n  /** What the agent wants, e.g. \"Kura wants to run a command\". */\n  title: React.ReactNode;\n  /** The exact command or action, shown verbatim in mono, e.g. \"rm -rf build\". */\n  command?: string;\n  /** One or two sentences of context: why, what it touches. */\n  description?: React.ReactNode;\n  /** Risk level, shown as a badge; `high` makes the approve button red. Default \"medium\". */\n  risk?: \"low\" | \"medium\" | \"high\";\n  /** Extra detail behind a “Details” expander (a diff, the full arguments, affected files…). */\n  details?: React.ReactNode;\n  /** Decision (controlled). */\n  decision?: ApprovalDecision;\n  /** Initial decision when uncontrolled. Default \"pending\". */\n  defaultDecision?: ApprovalDecision;\n  /** Called with every decision the user makes. */\n  onDecisionChange?: (decision: ApprovalDecision) => void;\n  onApprove?: () => void;\n  onDeny?: () => void;\n  /** Shows an “Always allow” action when provided. */\n  onAlwaysAllow?: () => void;\n  /** Bind ⌘/Ctrl+Enter to approve and Esc to deny while pending. Default true. Only one prompt on screen should bind them. */\n  shortcuts?: boolean;\n  /** Approve button label. Default “Approve” (“Run anyway” when risk is high). */\n  approveLabel?: string;\n  /** Deny button label. Default “Deny”. */\n  denyLabel?: string;\n  /** Always-allow button label. Default “Always allow”. */\n  alwaysAllowLabel?: string;\n  /** Icon in the header tile. Default a terminal. */\n  icon?: React.ReactNode;\n}\n\nconst riskLabel = { low: \"Low risk\", medium: \"Medium risk\", high: \"High risk\" } as const;\n\nconst resolvedCopy: Record<Exclude<ApprovalDecision, \"pending\">, string> = {\n  approved: \"Approved\",\n  denied: \"Denied\",\n  always: \"Always allowed\",\n};\n\n/**\n * Human-in-the-loop permission card: “Kura wants to run `rm -rf build`”, with a risk badge, a\n * details expander, Approve / Deny / Always allow, ⌘↵ and Esc shortcuts, and calm resolved states\n * that keep the card's height.\n */\nexport function ApprovalPrompt({\n  title,\n  command,\n  description,\n  risk = \"medium\",\n  details,\n  decision: decisionProp,\n  defaultDecision = \"pending\",\n  onDecisionChange,\n  onApprove,\n  onDeny,\n  onAlwaysAllow,\n  shortcuts = true,\n  approveLabel,\n  denyLabel = \"Deny\",\n  alwaysAllowLabel = \"Always allow\",\n  icon,\n  className,\n  ...props\n}: ApprovalPromptProps) {\n  const [decision, setDecision] = useControllableState<ApprovalDecision>({\n    value: decisionProp,\n    defaultValue: defaultDecision,\n    onChange: onDecisionChange,\n  });\n  const pending = decision === \"pending\";\n  const [open, setOpen] = React.useState(false);\n  const id = React.useId();\n\n  const approve = () => {\n    if (!pending) return;\n    setDecision(\"approved\");\n    onApprove?.();\n  };\n  const deny = () => {\n    if (!pending) return;\n    setDecision(\"denied\");\n    onDeny?.();\n  };\n  const always = () => {\n    if (!pending) return;\n    setDecision(\"always\");\n    onAlwaysAllow?.();\n  };\n\n  useHotkey([\"mod\", \"enter\"], approve, { enabled: shortcuts && pending });\n  useHotkey([\"escape\"], deny, { enabled: shortcuts && pending, allowInInputs: false });\n\n  return (\n    <section\n      aria-labelledby={command != null ? `${id}-title ${id}-code` : `${id}-title`}\n      aria-describedby={description != null ? `${id}-desc` : undefined}\n      data-slot=\"approval-prompt\"\n      data-decision={decision}\n      data-risk={risk}\n      className={cn(\n        \"flex flex-col overflow-hidden rounded-kura-xl bg-kura-surface font-kura-sans shadow-kura-md transition-shadow duration-(--kura-duration-slow) ease-kura-out\",\n        !pending && \"shadow-kura-sm\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex flex-col gap-3 p-4\">\n        <div className=\"flex items-start gap-3\">\n          <span\n            aria-hidden\n            className={cn(\n              \"grid size-8 shrink-0 place-items-center rounded-kura-md bg-kura-surface-sunken text-kura-fg-muted shadow-[inset_0_0_0_1px_var(--kura-border-subtle)] [&_svg]:size-4\",\n            )}\n          >\n            {icon ?? <Terminal />}\n          </span>\n          <div className=\"flex min-w-0 flex-1 flex-col gap-1 pt-px\">\n            <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n              <h3\n                id={`${id}-title`}\n                data-slot=\"approval-prompt-title\"\n                className={cn(\n                  \"font-medium text-[15px] text-kura-fg leading-snug transition-colors duration-(--kura-duration-base)\",\n                  !pending && \"text-kura-fg-muted\",\n                )}\n              >\n                {title}\n              </h3>\n              <span\n                data-slot=\"approval-prompt-risk\"\n                data-risk={risk}\n                className={cn(\n                  \"inline-flex h-5 items-center gap-1.5 rounded-full px-2 font-medium text-[11px]\",\n                  risk === \"low\" && \"bg-kura-hover text-kura-fg-muted\",\n                  risk === \"medium\" && \"bg-kura-hover text-kura-warning-text\",\n                  risk === \"high\" && \"bg-kura-danger-subtle text-kura-danger-text\",\n                )}\n              >\n                <span\n                  aria-hidden\n                  className={cn(\n                    \"size-1.5 rounded-full\",\n                    risk === \"low\" && \"bg-kura-success\",\n                    risk === \"medium\" && \"bg-kura-warning\",\n                    risk === \"high\" && \"bg-kura-danger\",\n                  )}\n                />\n                {riskLabel[risk]}\n              </span>\n            </div>\n            {description != null && (\n              <p id={`${id}-desc`} className=\"text-[13px] text-kura-fg-muted leading-relaxed\">\n                {description}\n              </p>\n            )}\n          </div>\n        </div>\n\n        {command != null && (\n          <div\n            data-slot=\"approval-prompt-command\"\n            className=\"flex min-w-0 gap-2 rounded-kura-md bg-kura-surface-sunken px-3 py-2.5 font-kura-mono text-[13px] shadow-[inset_0_0_0_1px_var(--kura-border-subtle)]\"\n          >\n            <span aria-hidden className=\"select-none text-kura-fg-subtle\">\n              $\n            </span>\n            <code\n              id={`${id}-code`}\n              data-slot=\"approval-prompt-code\"\n              className=\"min-w-0 flex-1 whitespace-pre-wrap break-all text-kura-fg\"\n            >\n              {command}\n            </code>\n          </div>\n        )}\n\n        {details != null && (\n          <div className=\"flex flex-col\">\n            <button\n              type=\"button\"\n              aria-expanded={open}\n              aria-controls={`${id}-details`}\n              onClick={() => setOpen((o) => !o)}\n              className=\"-mx-1.5 inline-flex h-6 w-fit cursor-pointer items-center gap-1 rounded-kura-sm px-1.5 text-kura-fg-muted text-xs outline-none transition-colors duration-(--kura-duration-fast) ease-kura-out hover:text-kura-fg focus-visible:shadow-kura-focus\"\n            >\n              <ChevronRight\n                aria-hidden\n                className={cn(\n                  \"size-3 transition-transform duration-(--kura-duration-base) ease-kura-out motion-reduce:transition-none\",\n                  open && \"rotate-90\",\n                )}\n              />\n              {open ? \"Hide details\" : \"Details\"}\n            </button>\n            <div\n              id={`${id}-details`}\n              data-slot=\"approval-prompt-details\"\n              inert={!open || undefined}\n              className={cn(\n                \"grid transition-[grid-template-rows,opacity] duration-(--kura-duration-slow) ease-kura-out motion-reduce:transition-opacity\",\n                open ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\",\n              )}\n            >\n              <div className=\"min-h-0 overflow-hidden\">\n                <div className=\"pt-2 text-[13px] text-kura-fg-muted\">{details}</div>\n              </div>\n            </div>\n          </div>\n        )}\n      </div>\n\n      <div\n        data-slot=\"approval-prompt-footer\"\n        className=\"relative flex h-14 items-center bg-kura-surface-sunken/60 px-4 shadow-[inset_0_1px_0_var(--kura-border-subtle)]\"\n      >\n        <div\n          data-slot=\"approval-prompt-actions\"\n          inert={!pending || undefined}\n          aria-hidden={!pending || undefined}\n          className={cn(\n            \"flex w-full items-center gap-2 transition-[opacity,filter] duration-(--kura-duration-fast) ease-kura-out\",\n            !pending && \"pointer-events-none opacity-0 blur-[2px] motion-reduce:blur-none\",\n          )}\n        >\n          <span\n            className=\"mr-auto inline-flex items-center gap-1.5 rounded-full bg-kura-accent-subtle px-2 py-0.5 font-medium text-kura-accent-text text-xs\"\n            data-slot=\"approval-prompt-waiting\"\n          >\n            <span\n              aria-hidden\n              className=\"size-1.5 animate-kura-pulse rounded-full bg-current motion-reduce:animate-none\"\n            />\n            Needs approval\n          </span>\n          {onAlwaysAllow && (\n            <Button variant=\"ghost\" size=\"sm\" onClick={always}>\n              {alwaysAllowLabel}\n            </Button>\n          )}\n          <Button\n            variant=\"secondary\"\n            size=\"sm\"\n            onClick={deny}\n            aria-keyshortcuts={shortcuts ? \"Escape\" : undefined}\n          >\n            {denyLabel}\n            {shortcuts && <Kbd keys={[\"esc\"]} variant=\"ghost\" size=\"sm\" className=\"-mr-1\" />}\n          </Button>\n          <Button\n            variant={risk === \"high\" ? \"danger\" : \"primary\"}\n            size=\"sm\"\n            onClick={approve}\n            aria-keyshortcuts={shortcuts ? \"Meta+Enter Control+Enter\" : undefined}\n          >\n            {approveLabel ?? (risk === \"high\" ? \"Run anyway\" : \"Approve\")}\n            {shortcuts && (\n              <Kbd\n                keys={[\"mod\", \"enter\"]}\n                variant=\"ghost\"\n                size=\"sm\"\n                className=\"-mr-1 [&_[data-slot=kbd-key]]:bg-white/15 [&_[data-slot=kbd-key]]:text-current\"\n              />\n            )}\n          </Button>\n        </div>\n\n        <div\n          data-slot=\"approval-prompt-resolution\"\n          role=\"status\"\n          className={cn(\n            \"absolute inset-0 flex items-center gap-2 px-4 text-[13px] transition-[opacity,filter] duration-(--kura-duration-base) ease-kura-out\",\n            pending\n              ? \"pointer-events-none opacity-0 blur-[2px] motion-reduce:blur-none\"\n              : \"opacity-100\",\n            decision === \"denied\" ? \"text-kura-fg-muted\" : \"text-kura-fg\",\n          )}\n        >\n          {!pending && (\n            <>\n              <span\n                aria-hidden\n                className={cn(\n                  \"grid size-5 animate-kura-pop-in place-items-center rounded-full [&_svg]:size-3\",\n                  decision === \"denied\"\n                    ? \"bg-kura-hover text-kura-fg-muted\"\n                    : \"bg-kura-hover text-kura-success-text\",\n                )}\n              >\n                {decision === \"denied\" ? (\n                  <X strokeWidth={2.5} />\n                ) : decision === \"always\" ? (\n                  <ShieldCheck />\n                ) : (\n                  <Check strokeWidth={2.5} />\n                )}\n              </span>\n              <span className=\"font-medium\">{resolvedCopy[decision]}</span>\n              {decision === \"always\" && (\n                <span className=\"text-kura-fg-subtle\">· won't ask again this session</span>\n              )}\n            </>\n          )}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
    },
    {
      "path": "registry/kura/approval-prompt/index.ts",
      "type": "registry:ui",
      "target": "components/kura/approval-prompt/index.ts",
      "content": "export {\n  type ApprovalDecision,\n  ApprovalPrompt,\n  type ApprovalPromptProps,\n} from \"./approval-prompt\";\n"
    }
  ],
  "categories": [
    "agent",
    "approval",
    "permission",
    "human-in-the-loop",
    "confirm"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/approval-prompt · Markdown for agents: https://kuro.abstergo.fyi/components/approval-prompt.md",
  "meta": {
    "kura": {
      "status": "beta",
      "whenToUse": [
        "An agent needs explicit permission before a side effect: running a shell command, editing files, sending email, spending money, calling a destructive API.",
        "Show exactly what will run (verbatim command) and how risky it is before the user decides.",
        "Offer “Always allow” for a class of actions in this session (persist the rule yourself in onAlwaysAllow).",
        "Keep the decision visible in the transcript afterwards (Approved / Denied / Always allowed) without the card changing height."
      ],
      "whenNotToUse": [
        "Showing a tool call that does not need permission — use tool-call (it also has an awaiting-approval status for compact inline approvals).",
        "Generic confirm dialogs unrelated to an agent (deleting a document) — use a dialog.",
        "Progress of a multi-step plan — use agent-timeline."
      ]
    }
  }
}
