{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "approval-prompt",
  "title": "ApprovalPrompt",
  "description": "One specific action to allow or deny. A, Shift+A and D work while focus is inside it.",
  "registryDependencies": [
    "https://creance.pages.dev/r/badge.json",
    "https://creance.pages.dev/r/button.json",
    "https://creance.pages.dev/r/code.json",
    "https://creance.pages.dev/r/creance-base.json",
    "https://creance.pages.dev/r/keyboard-hint.json",
    "https://creance.pages.dev/r/risk-label.json"
  ],
  "files": [
    {
      "path": "approval-prompt/ApprovalPrompt.css",
      "content": ".cr-approval {\n  display: grid;\n  gap: var(--cr-space-3);\n  padding: var(--cr-space-6);\n  border-radius: var(--cr-radius-xl);\n  color: var(--cr-color-text-default);\n  font-family: var(--cr-font-family-sans);\n  font-size: var(--cr-font-size-md);\n}\n\n.cr-approval[data-risk='high'] {\n  box-shadow: var(--cr-shadow-sheet), inset 0 0 0 1.5px var(--cr-color-danger-text);\n}\n\n.cr-approval[data-resolved] {\n  box-shadow: var(--cr-shadow-card);\n}\n\n.cr-approval:focus-visible {\n  outline: var(--cr-focus-width) solid var(--cr-color-focus);\n  outline-offset: var(--cr-focus-offset);\n}\n\n.cr-approval__head {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: flex-start;\n  justify-content: space-between;\n  gap: var(--cr-space-2);\n}\n\n.cr-approval__title {\n  margin: 0;\n  font-family: var(--cr-font-family-display);\n  font-size: 28px;\n  font-weight: var(--cr-font-weight-bold);\n  letter-spacing: var(--cr-font-tracking-title);\n  line-height: 1.2;\n}\n\n.cr-approval__title .cr-code {\n  padding: 0;\n  background: none;\n  color: var(--cr-color-text-default);\n  font-size: 0.86em;\n  font-weight: var(--cr-font-weight-semibold);\n  letter-spacing: 0;\n}\n\n.cr-approval__reason {\n  margin: 0;\n  max-width: 64ch;\n  color: var(--cr-color-text-secondary);\n  line-height: var(--cr-font-line-height-normal);\n}\n\n.cr-approval__actions {\n  display: flex;\n  flex-wrap: wrap;\n  gap: var(--cr-space-2);\n  margin-top: var(--cr-space-1);\n}\n\n/* Hints inside buttons inherit the button colour so they stay readable on filled variants. */\n.cr-approval__actions .cr-kbd-hint {\n  color: inherit;\n}\n\n.cr-approval__actions .cr-kbd {\n  border-color: color-mix(in srgb, currentColor 40%, transparent);\n}\n\n.cr-approval__outcome {\n  margin: 0;\n}\n\n.cr-approval__outcome:focus-visible {\n  outline: var(--cr-focus-width) solid var(--cr-color-focus);\n  outline-offset: var(--cr-focus-offset);\n}\n",
      "type": "registry:file",
      "target": "@components/creance/approval-prompt/ApprovalPrompt.css"
    },
    {
      "path": "approval-prompt/ApprovalPrompt.tsx",
      "content": "'use client';\n\nimport { useEffect, useId, useRef, type KeyboardEvent } from 'react';\nimport { Badge } from '@/components/creance/badge/Badge';\nimport { Button } from '@/components/creance/button/Button';\nimport { Code } from '@/components/creance/code/Code';\nimport { KeyboardHint } from '@/components/creance/keyboard-hint/KeyboardHint';\nimport { RiskLabel, type Risk } from '@/components/creance/risk-label/RiskLabel';\nimport { cx } from '@/lib/creance/cx';\nimport { deprecate } from '@/lib/creance/deprecate';\nimport { LiveRegion } from '@/lib/creance/LiveRegion';\nimport './ApprovalPrompt.css';\n\nexport type ApprovalDecision = 'allow-once' | 'always-allow' | 'deny';\n\nexport interface ApprovalPromptProps {\n  /** The exact command or operation, e.g. \"npm install zod@3\". */\n  action: string;\n  /** What it acts on, e.g. a file, repo or environment. */\n  target?: string | undefined;\n  risk: Risk;\n  /** Why the agent wants to do this, in the agent's words. */\n  reason: string;\n  agentName?: string | undefined;\n  /** Called with the scope the person allowed: just this once, or always. */\n  onAllow?: (scope: 'once' | 'always') => void;\n  /**\n   * @deprecated Renamed to `onAllow` in 0.2.0, to match the \"Allow\" buttons. Still works, with a\n   * development warning; removed in 1.0. Run `npx creance-codemod-on-allow src` to migrate.\n   */\n  onApprove?: (scope: 'once' | 'always') => void;\n  onDeny?: () => void;\n  /** Once set, the prompt shows the outcome instead of the buttons. */\n  decision?: ApprovalDecision | undefined;\n  /** Offer \"Always allow\". Defaults to false for high risk: a dangerous action should be confirmed every time. */\n  allowAlways?: boolean;\n  /** Move focus to the prompt when it appears. Default true; turn off when several prompts render at once. */\n  autoFocus?: boolean;\n  /** A, Shift+A and D while focus is inside the prompt. Default true. */\n  shortcuts?: boolean;\n  headingLevel?: 2 | 3 | 4;\n  className?: string;\n}\n\nconst outcome: Record<ApprovalDecision, { text: string; status: 'done' | 'failed' }> = {\n  'allow-once': { text: 'Allowed once', status: 'done' },\n  'always-allow': { text: 'Always allowed', status: 'done' },\n  deny: { text: 'Denied', status: 'failed' },\n};\n\nexport function ApprovalPrompt({\n  action,\n  target,\n  risk,\n  reason,\n  agentName = 'The agent',\n  onAllow,\n  onApprove,\n  onDeny,\n  decision,\n  allowAlways = risk !== 'high',\n  autoFocus = true,\n  shortcuts = true,\n  headingLevel = 3,\n  className,\n}: ApprovalPromptProps) {\n  const titleId = useId();\n  const reasonId = useId();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const outcomeRef = useRef<HTMLParagraphElement>(null);\n  const Heading = `h${headingLevel}` as const;\n\n  const usesOnApprove = onApprove !== undefined;\n  useEffect(() => {\n    if (usesOnApprove) {\n      deprecate(\n        'ApprovalPrompt.onApprove',\n        'ApprovalPrompt: `onApprove` is deprecated and will be removed in 1.0. Rename it to `onAllow`. See docs/MIGRATIONS.md.',\n      );\n    }\n  }, [usesOnApprove]);\n  const allow = onAllow ?? onApprove;\n\n  useEffect(() => {\n    if (autoFocus && !decision) rootRef.current?.focus();\n    // Only on mount: re-focusing on every render would steal focus from the buttons.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // After a decision the buttons disappear; keep focus in the prompt instead of dropping it to <body>.\n  useEffect(() => {\n    const active = document.activeElement;\n    if (decision && (active === document.body || rootRef.current?.contains(active))) outcomeRef.current?.focus();\n  }, [decision]);\n\n  function onKeyDown(e: KeyboardEvent<HTMLDivElement>) {\n    if (!shortcuts || decision || e.metaKey || e.ctrlKey || e.altKey) return;\n    if ((e.target as HTMLElement).closest('input, textarea, select, [contenteditable=\"true\"]')) return;\n    const key = e.key.toLowerCase();\n    if (key === 'a' && e.shiftKey && allowAlways) allow?.('always');\n    else if (key === 'a' && !e.shiftKey) allow?.('once');\n    else if (key === 'd' && !e.shiftKey) onDeny?.();\n    else return;\n    e.preventDefault();\n  }\n\n  const announcement = decision\n    ? `${outcome[decision].text}: ${action}`\n    : `Approval needed. ${agentName} wants to run ${action}. ${risk} risk.`;\n\n  return (\n    // A labelled group, not a modal: the person can still read the run before deciding.\n    // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n    <div\n      ref={rootRef}\n      role=\"group\"\n      aria-labelledby={titleId}\n      aria-describedby={reasonId}\n      tabIndex={-1}\n      onKeyDown={onKeyDown}\n      className={cx('cr-approval cr-glass', className)}\n      data-risk={risk}\n      data-resolved={decision ? true : undefined}\n    >\n      <div className=\"cr-approval__head\">\n        <Heading id={titleId} className=\"cr-approval__title\">\n          {agentName} wants to run <Code>{action}</Code>\n          {target && (\n            <>\n              {' '}\n              on <Code>{target}</Code>\n            </>\n          )}\n        </Heading>\n        <RiskLabel level={risk} />\n      </div>\n      <p id={reasonId} className=\"cr-approval__reason\">\n        {reason}\n      </p>\n\n      {decision ? (\n        <p ref={outcomeRef} tabIndex={-1} className=\"cr-approval__outcome\">\n          <Badge status={outcome[decision].status}>{outcome[decision].text}</Badge>\n        </p>\n      ) : (\n        <div className=\"cr-approval__actions\">\n          <Button variant=\"primary\" onClick={() => allow?.('once')} aria-keyshortcuts={shortcuts ? 'A' : undefined}>\n            Allow once {shortcuts && <KeyboardHint keys={['A']} />}\n          </Button>\n          {allowAlways && (\n            <Button onClick={() => allow?.('always')} aria-keyshortcuts={shortcuts ? 'Shift+A' : undefined}>\n              Always allow {shortcuts && <KeyboardHint keys={['Shift', 'A']} />}\n            </Button>\n          )}\n          <Button variant=\"danger\" onClick={() => onDeny?.()} aria-keyshortcuts={shortcuts ? 'D' : undefined}>\n            Deny {shortcuts && <KeyboardHint keys={['D']} />}\n          </Button>\n        </div>\n      )}\n      <LiveRegion message={announcement} />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/creance/approval-prompt/ApprovalPrompt.tsx"
    }
  ],
  "categories": [
    "organism",
    "agent"
  ],
  "type": "registry:ui"
}