{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "engine-telegraph",
  "title": "Engine telegraph",
  "description": "A ship's engine order telegraph: you set an order, the other side answers by moving its own pointer to match. Both show on one dial, so a pending order is visible at a glance. For autonomy levels, deploy stages and any setting that takes effect only once acknowledged.",
  "dependencies": [
    "@base-ui/react",
    "cn"
  ],
  "registryDependencies": [
    "https://bevel-ui.vercel.app/r/bevel.json"
  ],
  "files": [
    {
      "path": "registry/bevel/ui/engine-telegraph.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Radio } from \"@base-ui/react/radio\"\nimport { RadioGroup } from \"@base-ui/react/radio-group\"\nimport { cn } from \"cn\"\n\ntype Order = { value: string; label: string }\n\ntype EngineTelegraphContextValue = {\n  orders: Order[]\n  register: (order: Order) => () => void\n  value: string\n  confirmed: string\n  statusId: string\n}\n\nconst EngineTelegraphContext = React.createContext<EngineTelegraphContextValue | null>(null)\n\nfunction useEngineTelegraph() {\n  const context = React.useContext(EngineTelegraphContext)\n  if (!context) throw new Error(\"EngineTelegraph parts must be inside <EngineTelegraph>\")\n  return context\n}\n\n/* Orders spread over the top of the dial, from -70° to 70°. */\nconst SPREAD = 140\n\nfunction angleOf(orders: Order[], value: string) {\n  const i = orders.findIndex((o) => o.value === value)\n  if (i === -1 || orders.length < 2) return 0\n  return -SPREAD / 2 + (SPREAD * i) / (orders.length - 1)\n}\n\ntype EngineTelegraphProps = Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n  /** The order you've set. */\n  value: string\n  onValueChange?: (value: string) => void\n  /** The order the other side has acknowledged. Differs from value while an order is pending. */\n  confirmed: string\n}\n\n/**\n * An engine order telegraph. You set an order; the other side (an agent,\n * a service, a teammate) answers by confirming it. Both pointers show on\n * one dial, so a pending order is visible at a glance.\n */\nfunction EngineTelegraph({ className, value, onValueChange, confirmed, children, ...props }: EngineTelegraphProps) {\n  const [orders, setOrders] = React.useState<Order[]>([])\n  const statusId = React.useId()\n  const register = React.useCallback((order: Order) => {\n    // Orders keep the order they mounted in, which is their order on the dial.\n    setOrders((all) =>\n      all.some((o) => o.value === order.value)\n        ? all.map((o) => (o.value === order.value ? order : o))\n        : [...all, order]\n    )\n    return () => setOrders((all) => all.filter((o) => o.value !== order.value))\n  }, [])\n  const context = React.useMemo(\n    () => ({ orders, register, value, confirmed, statusId }),\n    [orders, register, value, confirmed, statusId]\n  )\n  return (\n    <EngineTelegraphContext value={context}>\n      <div\n        role=\"group\"\n        data-slot=\"engine-telegraph\"\n        data-pending={value !== confirmed ? \"\" : undefined}\n        className={cn(\"flex w-full max-w-sm flex-col items-center gap-4\", className)}\n        {...props}\n      >\n        <RadioGroup\n          value={value}\n          onValueChange={(next) => onValueChange?.(next as string)}\n          aria-describedby={statusId}\n          className=\"contents\"\n        >\n          {children}\n        </RadioGroup>\n      </div>\n    </EngineTelegraphContext>\n  )\n}\n\n/** The dial: your handle and the answering pointer, over the orders. */\nfunction EngineTelegraphDial({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { orders, value, confirmed } = useEngineTelegraph()\n  return (\n    <div\n      aria-hidden\n      data-slot=\"engine-telegraph-dial\"\n      className={cn(\"bevel-metal w-full rounded-t-full p-2 pb-0 shadow-(--bevel-depth-object)\", className)}\n      {...props}\n    >\n      <div className=\"relative overflow-hidden rounded-t-full bg-(--bevel-ivory) text-(--bevel-ivory-foreground) shadow-(--bevel-depth-recessed)\">\n        <svg viewBox=\"0 0 100 60\" className=\"block w-full\">\n          {orders.map((order) => {\n            const angle = angleOf(orders, order.value)\n            return (\n              <g key={order.value} transform={`rotate(${angle} 50 56)`}>\n                <line x1=\"50\" y1=\"6\" x2=\"50\" y2=\"12\" stroke=\"currentColor\" strokeWidth=\"0.8\" />\n                <text x=\"50\" y=\"19\" textAnchor=\"middle\" fontSize=\"4.6\" fontWeight=\"700\" fill=\"currentColor\">\n                  {order.label.toUpperCase()}\n                </text>\n              </g>\n            )\n          })}\n          {/* The answer pointer: short and red, moved by the other side. */}\n          <g\n            className=\"origin-(--engine-telegraph-pivot) transition-[rotate] duration-(--bevel-duration-ambient) ease-(--bevel-ease-spring) motion-reduce:transition-none\"\n            style={{ rotate: `${angleOf(orders, confirmed)}deg` }}\n          >\n            <path d=\"M48.6 56 L50 26 L51.4 56 Z\" fill=\"var(--engine-telegraph-answer)\" />\n          </g>\n          {/* Your handle: long and dark, moved by you. */}\n          <g\n            className=\"origin-(--engine-telegraph-pivot) transition-[rotate] duration-(--bevel-duration-move) ease-(--bevel-ease-out) motion-reduce:transition-none\"\n            style={{ rotate: `${angleOf(orders, value)}deg` }}\n          >\n            <path d=\"M49.2 56 L50 22 L50.8 56 Z\" fill=\"currentColor\" />\n            <rect x=\"47.5\" y=\"21\" width=\"5\" height=\"3\" rx=\"1\" fill=\"currentColor\" />\n          </g>\n          <circle cx=\"50\" cy=\"56\" r=\"4\" fill=\"var(--bevel-brass)\" stroke=\"var(--bevel-brass-edge)\" strokeWidth=\"0.6\" />\n        </svg>\n        <span className=\"bevel-glass absolute inset-0\" />\n      </div>\n    </div>\n  )\n}\n\n/** The orders you can give: a row of plates. */\nfunction EngineTelegraphOrders({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"engine-telegraph-orders\" className={cn(\"flex flex-wrap justify-center gap-2\", className)} {...props} />\n}\n\ntype EngineTelegraphOrderProps = Omit<Radio.Root.Props, \"value\" | \"children\"> & {\n  value: string\n  /** Shown on the plate and the dial. Keep it to a word. */\n  children: string\n}\n\n/** One order. A radio: arrow keys move between orders. */\nfunction EngineTelegraphOrder({ className, value, children, ...props }: EngineTelegraphOrderProps) {\n  const { register, confirmed } = useEngineTelegraph()\n  React.useEffect(() => register({ value, label: children }), [value, children, register])\n  return (\n    <Radio.Root\n      value={value}\n      data-slot=\"engine-telegraph-order\"\n      data-confirmed={confirmed === value ? \"\" : undefined}\n      className={cn(\n        \"bevel-raised flex h-9 min-w-14 items-center justify-center rounded-(--bevel-radius-sm) px-3 text-xs font-bold tracking-(--bevel-tracking-label) text-(--bevel-ivory-foreground) uppercase outline-none select-none\",\n        \"transition-[translate,box-shadow] duration-(--bevel-duration-press) ease-(--bevel-ease-out) motion-reduce:transition-none\",\n        \"data-checked:translate-y-0.5 data-checked:bevel-pressed\",\n        \"focus-visible:ring-3 focus-visible:ring-ring/60 data-disabled:cursor-not-allowed data-disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </Radio.Root>\n  )\n}\n\n/** Whether the other side has answered, in words. Announced when that changes. */\nfunction EngineTelegraphStatus({ className, ...props }: React.ComponentProps<\"p\">) {\n  const { orders, value, confirmed, statusId } = useEngineTelegraph()\n  const label = (v: string) => orders.find((o) => o.value === v)?.label ?? v\n  return (\n    <p\n      id={statusId}\n      role=\"status\"\n      data-slot=\"engine-telegraph-status\"\n      className={cn(\"flex items-center gap-2 text-sm text-muted-foreground\", className)}\n      {...props}\n    >\n      <span aria-hidden className=\"bevel-led size-2 rounded-full\" />\n      {value === confirmed\n        ? `Confirmed: ${label(confirmed)}.`\n        : `Ordered: ${label(value)}. Waiting for confirmation (now at ${label(confirmed)}).`}\n    </p>\n  )\n}\n\nexport { EngineTelegraph, EngineTelegraphDial, EngineTelegraphOrders, EngineTelegraphOrder, EngineTelegraphStatus }\nexport type { EngineTelegraphProps, EngineTelegraphOrderProps }\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    ":root": {
      "--engine-telegraph-answer": "oklch(0.55 0.2 27)",
      "--engine-telegraph-pending": "oklch(0.78 0.16 75)",
      "--engine-telegraph-agreed": "oklch(0.72 0.16 150)",
      "--engine-telegraph-pivot": "50px 56px"
    },
    "[data-slot=\"engine-telegraph\"]": {
      "--bevel-material": "var(--bevel-brass)",
      "--bevel-material-edge": "var(--bevel-brass-edge)",
      "--bevel-led": "var(--engine-telegraph-agreed)"
    },
    "[data-slot=\"engine-telegraph\"][data-pending]": {
      "--bevel-led": "var(--engine-telegraph-pending)"
    }
  },
  "categories": [
    "instruments"
  ],
  "type": "registry:ui"
}