{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "autonomy-dial",
  "title": "Autonomy Dial",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A stepped dial for how much an agent may do on its own, from suggest-only to full auto, with what it does alone and what it stops to ask about written out for every step.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/autonomy-dial.tsx",
      "content": "// Ballmac UI: Autonomy Dial. https://ui.ballmac.com/components/autonomy-dial\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, HelpCircle } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype AutonomyLevel = {\n  /** Stable value reported to onValueChange. */\n  value: string\n  /** Short name shown under the dial, for example \"Ask first\". */\n  label: string\n  /** One sentence on what this setting means. */\n  summary: string\n  /** What the agent does without asking at this level. */\n  does: string[]\n  /** What it stops to ask about at this level. */\n  asks: string[]\n}\n\n/** Four settings from \"suggest only\" to \"full auto\", written to be edited. */\nconst defaultAutonomyLevels: AutonomyLevel[] = [\n  {\n    value: \"suggest\",\n    label: \"Suggest\",\n    summary: \"Reads and proposes. Nothing changes until you do it yourself.\",\n    does: [\"Reads files and searches\", \"Explains and proposes changes\"],\n    asks: [\"Every edit\", \"Every command\", \"Anything on the network\"],\n  },\n  {\n    value: \"ask\",\n    label: \"Ask first\",\n    summary: \"Does the work after you approve each step.\",\n    does: [\"Reads files and searches\", \"Prepares edits and commands\"],\n    asks: [\"Before each edit\", \"Before each command\", \"Before using the network\"],\n  },\n  {\n    value: \"auto-edit\",\n    label: \"Auto-edit\",\n    summary: \"Edits files freely, and asks before anything that runs or reaches out.\",\n    does: [\"Reads and edits files in the project\", \"Runs checks you have allowed\"],\n    asks: [\"Other commands\", \"Installing packages\", \"Using the network\"],\n  },\n  {\n    value: \"full\",\n    label: \"Full auto\",\n    summary: \"Works on its own inside its sandbox and stops only for what cannot be undone.\",\n    does: [\"Edits files and runs commands\", \"Installs packages inside the sandbox\"],\n    asks: [\"Deleting data\", \"Anything outside the sandbox\", \"Spending past the budget\"],\n  },\n]\n\ntype AutonomyDialProps = Omit<React.ComponentProps<\"fieldset\">, \"onChange\" | \"defaultValue\"> & {\n  /** The settings, from least to most autonomous. Defaults to four general ones. */\n  levels?: AutonomyLevel[]\n  /** The selected value. Controlled. */\n  value?: string\n  /** The selected value for uncontrolled use. */\n  defaultValue?: string\n  /** Called with the new value. */\n  onValueChange?: (value: string) => void\n  /** Heading above the dial. */\n  label?: string\n}\n\n/**\n * How much an agent may do on its own, as a stepped dial with what each step means written out underneath. The steps are a native radio group, so\n * the arrow keys, Home, End and the browser's own right-to-left handling all work. The track is decoration; the words carry the meaning.\n */\nfunction AutonomyDial({ levels = defaultAutonomyLevels, value: valueProp, defaultValue, onValueChange, label, className, ...props }: AutonomyDialProps) {\n  const msg = useMessages()\n  const name = React.useId()\n  label ??= msg(\"autonomy-dial.label\", \"How much can the agent do on its own?\")\n  const [inner, setInner] = React.useState(defaultValue ?? levels[0]?.value)\n  const value = valueProp ?? inner\n  const index = Math.max(0, levels.findIndex((l) => l.value === value))\n  const current = levels[index]\n  const last = Math.max(1, levels.length - 1)\n\n  function pick(v: string) {\n    if (valueProp === undefined) setInner(v)\n    onValueChange?.(v)\n  }\n\n  if (!current) return null\n  return (\n    <fieldset data-slot=\"autonomy-dial\" className={cn(\"grid w-full min-w-0 gap-6 rounded-xl border bg-card p-5 text-card-foreground sm:p-6\", className)} {...props}>\n      <legend className=\"sr-only\">{label}</legend>\n      <div className=\"flex items-baseline justify-between gap-3\" aria-hidden=\"true\">\n        <p className=\"text-sm font-medium text-foreground\">{label}</p>\n        <p className=\"font-mono text-xs text-muted-foreground tabular-nums\">\n          {index + 1} / {levels.length}\n        </p>\n      </div>\n\n      <div className=\"relative px-4\">\n        {/* The track and its fill. Decoration: each step below is a labelled radio. */}\n        <div aria-hidden=\"true\" className=\"absolute inset-x-4 top-[0.9rem] h-1.5 -translate-y-1/2 rounded-full bg-muted\">\n          <div className=\"h-full rounded-full bg-foreground transition-[width] duration-300 ease-out motion-reduce:transition-none\" style={{ width: `${(index / last) * 100}%` }} />\n        </div>\n        <div className=\"relative flex justify-between\">\n          {levels.map((l, i) => {\n            const on = l.value === current.value\n            const passed = i <= index\n            return (\n              <label key={l.value} className=\"group/step relative flex w-0 cursor-pointer flex-col items-center\">\n                <input type=\"radio\" name={name} value={l.value} checked={on} onChange={() => pick(l.value)} className=\"peer sr-only\" />\n                <span\n                  aria-hidden=\"true\"\n                  className={cn(\n                    \"flex size-[1.8rem] items-center justify-center rounded-full border-2 bg-card transition-[transform,background-color,border-color] duration-200 motion-reduce:transition-none\",\n                    passed ? \"border-foreground\" : \"border-border group-hover/step:border-foreground/50\",\n                    on && \"scale-110 bg-foreground text-background\",\n                    \"peer-focus-visible:ring-[3px] peer-focus-visible:ring-ring/50 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-card\"\n                  )}\n                >\n                  {passed ? <Check className=\"size-3.5\" /> : <span className=\"size-1.5 rounded-full bg-muted-foreground/60\" />}\n                </span>\n                <span className={cn(\"mt-2.5 w-24 text-center text-[13px] leading-tight text-balance\", on ? \"font-semibold text-foreground\" : \"text-muted-foreground\")}>{l.label}</span>\n              </label>\n            )\n          })}\n        </div>\n      </div>\n\n      <div aria-live=\"polite\" className=\"grid gap-4 rounded-lg border bg-background/60 p-4\">\n        <p className=\"text-sm leading-relaxed text-foreground\">\n          <span className=\"font-semibold\">{current.label}.</span> {current.summary}\n        </p>\n        <div className=\"grid gap-4 sm:grid-cols-2\">\n          <div>\n            <p className=\"flex items-center gap-1.5 text-xs font-medium text-muted-foreground\">\n              <Check aria-hidden=\"true\" className=\"size-3.5\" /> {msg(\"autonomy-dial.does\", \"Does on its own\")}\n            </p>\n            <ul className=\"mt-2 grid gap-1.5 text-[13px] leading-snug\">\n              {current.does.map((t) => (\n                <li key={t} className=\"flex gap-2\">\n                  <span aria-hidden=\"true\" className=\"mt-[0.45rem] size-1 shrink-0 rounded-full bg-foreground\" />\n                  {t}\n                </li>\n              ))}\n            </ul>\n          </div>\n          <div>\n            <p className=\"flex items-center gap-1.5 text-xs font-medium text-muted-foreground\">\n              <HelpCircle aria-hidden=\"true\" className=\"size-3.5\" /> {msg(\"autonomy-dial.asks\", \"Stops to ask about\")}\n            </p>\n            <ul className=\"mt-2 grid gap-1.5 text-[13px] leading-snug\">\n              {current.asks.map((t) => (\n                <li key={t} className=\"flex gap-2\">\n                  <span aria-hidden=\"true\" className=\"mt-[0.45rem] size-1 shrink-0 rounded-full border border-foreground\" />\n                  {t}\n                </li>\n              ))}\n            </ul>\n          </div>\n        </div>\n      </div>\n    </fieldset>\n  )\n}\n\nexport { AutonomyDial, defaultAutonomyLevels, type AutonomyDialProps, type AutonomyLevel }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/autonomy-dial.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "agents",
      "permissions",
      "autonomy",
      "settings",
      "control",
      "safety"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "levels is [{ value, label, summary, does: string[], asks: string[] }] from least to most autonomous (defaultAutonomyLevels has four). value / defaultValue / onValueChange. A native radio group, so arrow keys work; the panel under the dial says in words what the chosen level allows.",
      "whenToUse": [
        "Letting people decide how far an agent may go before asking",
        "A settings row for an assistant's permissions"
      ],
      "whenNotToUse": [
        "A single yes or no approval; use approval-card",
        "A numeric value; use slider"
      ],
      "composesWith": [
        "approval-card",
        "settings-panel",
        "agent-plan",
        "tool-call-card"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Moves into the dial once, onto the selected step"
        },
        {
          "keys": "ArrowRight / ArrowDown",
          "action": "Next level (native radio behaviour; the browser mirrors it in right-to-left)"
        },
        {
          "keys": "ArrowLeft / ArrowUp",
          "action": "Previous level"
        },
        {
          "keys": "Screen readers",
          "action": "A fieldset with a legend, one radio per level, and a polite live region that reads the new meaning when it changes"
        },
        {
          "keys": "Meaning",
          "action": "Carried by the words under the dial, not by the track or its colour"
        }
      ],
      "customization": [
        "levels: any number of steps, your own words",
        "label",
        "Controlled or uncontrolled value"
      ]
    },
    "statesNone": "A control with no data of its own: it shows the levels you pass in and reports the choice.",
    "examples": [
      "autonomy-dial-demo",
      "autonomy-dial-custom"
    ],
    "url": "https://ui.ballmac.com/components/autonomy-dial"
  },
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}