{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "arm-controls",
  "title": "Controls",
  "description": "A teach pendant: one slider per joint driving an arm in forward kinematics, plus the tool selector and live readouts.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://robocn.dev/r/robot-style.json",
    "button",
    "card",
    "label",
    "select",
    "slider",
    "https://robocn.dev/r/use-robot-motion.json"
  ],
  "files": [
    {
      "path": "src/components/ui/arm-controls.tsx",
      "content": "\"use client\"\n\n/**\n * arm-controls — a teach pendant.\n *\n * One slider per joint, driving an arm in forward kinematics: pass the same\n * `angles` array to `RobotArm` and the arm is posed joint by joint instead of\n * chasing a target. Also carries the tool selector and live readouts, because\n * that is what sits next to the sliders on a real pendant.\n *\n * A pendant also runs programs, so this one does: play, and it drives the same\n * `onAnglesChange` a slider does, frame by frame. Touch a slider and playback\n * stops — whoever has their hand on the machine has control of it.\n */\n\nimport * as React from \"react\"\nimport { Pause, Play, RotateCcw } from \"lucide-react\"\n\nimport { useRobotClock } from \"@/hooks/use-robot-motion\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport { Label } from \"@/components/ui/label\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { Slider } from \"@/components/ui/slider\"\nimport type { RobotTool } from \"@/lib/robocn/style\"\nimport { cn } from \"@/lib/utils\"\n\nconst ALL_TOOLS: RobotTool[] = [\n  \"gripper\",\n  \"welder\",\n  \"painter\",\n  \"cutter\",\n  \"scanner\",\n  \"vacuum\",\n  \"magnet\",\n  \"none\",\n]\n\nexport interface ArmControlsProps\n  extends Omit<React.ComponentProps<\"div\">, \"onChange\"> {\n  /** One angle per joint, in degrees, relative to the previous segment. */\n  angles: number[]\n  onAnglesChange: (angles: number[]) => void\n  /** One range for every joint, or a range each. */\n  limits?: [number, number] | [number, number][]\n  /** Joint names. Defaults to J1, J2, … */\n  labels?: string[]\n  title?: string\n  description?: string\n  tool?: RobotTool\n  onToolChange?: (tool: RobotTool) => void\n  /** Which tools to offer. Defaults to all of them. */\n  tools?: RobotTool[]\n  /** Extra values to show under the sliders, e.g. the tip position. */\n  readouts?: { label: string; value: React.ReactNode }[]\n  /** Shown as a reset button when provided. */\n  onReset?: () => void\n  /**\n   * Offer a play button that runs `program` into `onAnglesChange`. Moving a\n   * slider stops it.\n   */\n  playable?: boolean\n  /** Joint angles at `clock` seconds. Defaults to a sweep of every joint. */\n  program?: (clock: number, limits: [number, number][]) => number[]\n  /** Program cycles per second. */\n  speed?: number\n  playing?: boolean\n  onPlayingChange?: (playing: boolean) => void\n}\n\nfunction ArmControls({\n  angles,\n  onAnglesChange,\n  limits = [-180, 180],\n  labels,\n  title = \"Teach pendant\",\n  description,\n  tool,\n  onToolChange,\n  tools = ALL_TOOLS,\n  readouts,\n  onReset,\n  playable = false,\n  program = jointSweep,\n  speed = 0.2,\n  playing: playingProp,\n  onPlayingChange,\n  className,\n  ...props\n}: ArmControlsProps) {\n  const rangeFor = (index: number): [number, number] =>\n    Array.isArray(limits[0])\n      ? ((limits as [number, number][])[index] ?? [-180, 180])\n      : (limits as [number, number])\n  const ranges = angles.map((_, index) => rangeFor(index))\n\n  const [playingState, setPlayingState] = React.useState(false)\n  const playing = playingProp ?? playingState\n  const setPlaying = React.useCallback((next: boolean) => {\n    setPlayingState(next)\n    onPlayingChange?.(next)\n  }, [onPlayingChange, setPlayingState])\n\n  const clock = useRobotClock({ speed, animate: playable && playing })\n  // The program drives the same callback the sliders do, so nothing downstream\n  // has to know whether a person or the pendant is moving the arm.\n  const posed = React.useRef<number[] | null>(null)\n  React.useEffect(() => {\n    if (!playable || !playing) return\n    const next = program(clock, ranges)\n    if (posed.current && next.every((angle, i) => angle === posed.current![i])) return\n    posed.current = next\n    onAnglesChange(next)\n  })\n\n  return (\n    <Card className={cn(\"w-full max-w-xs\", className)} {...props}>\n      <CardHeader>\n        <CardTitle className=\"text-sm font-medium\">{title}</CardTitle>\n        {description ? (\n          <CardDescription>{description}</CardDescription>\n        ) : null}\n      </CardHeader>\n      <CardContent className=\"space-y-5\">\n        {angles.map((angle, index) => {\n          const [min, max] = rangeFor(index)\n          const name = labels?.[index] ?? `J${index + 1}`\n          return (\n            <div key={index} className=\"space-y-2\">\n              <div className=\"flex items-center justify-between\">\n                <Label htmlFor={`joint-${index}`} className=\"text-xs\">\n                  {name}\n                </Label>\n                <span className=\"font-mono text-xs tabular-nums text-muted-foreground\">\n                  {angle.toFixed(0)}°\n                </span>\n              </div>\n              <Slider\n                id={`joint-${index}`}\n                min={min}\n                max={max}\n                step={1}\n                value={[angle]}\n                onValueChange={(next) => {\n                  // A hand on a slider outranks the program.\n                  if (playing) setPlaying(false)\n                  const updated = [...angles]\n                  updated[index] = Array.isArray(next) ? next[0] : next\n                  onAnglesChange(updated)\n                }}\n                aria-label={`${name} angle in degrees`}\n              />\n            </div>\n          )\n        })}\n\n        {onToolChange ? (\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"tool\" className=\"text-xs\">\n              End effector\n            </Label>\n            <Select\n              value={tool}\n              onValueChange={(next) => onToolChange(next as RobotTool)}\n            >\n              <SelectTrigger id=\"tool\" className=\"w-full\" size=\"sm\">\n                <SelectValue placeholder=\"Tool\" />\n              </SelectTrigger>\n              <SelectContent>\n                {tools.map((option) => (\n                  <SelectItem key={option} value={option}>\n                    {option}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </div>\n        ) : null}\n\n        {readouts?.length ? (\n          <dl className=\"grid grid-cols-2 gap-x-3 gap-y-1 border-t pt-3 text-xs\">\n            {readouts.map((readout) => (\n              <React.Fragment key={readout.label}>\n                <dt className=\"text-muted-foreground\">{readout.label}</dt>\n                <dd className=\"text-right font-mono tabular-nums\">\n                  {readout.value}\n                </dd>\n              </React.Fragment>\n            ))}\n          </dl>\n        ) : null}\n\n        {playable || onReset ? (\n          <div className=\"flex gap-2\">\n            {playable ? (\n              <Button\n                variant={playing ? \"secondary\" : \"outline\"}\n                size=\"sm\"\n                className=\"flex-1\"\n                aria-pressed={playing}\n                onClick={() => setPlaying(!playing)}\n              >\n                {playing ? <Pause /> : <Play />}\n                {playing ? \"Stop\" : \"Run\"}\n              </Button>\n            ) : null}\n            {onReset ? (\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                className=\"flex-1\"\n                onClick={() => {\n                  if (playing) setPlaying(false)\n                  onReset()\n                }}\n              >\n                <RotateCcw />\n                Home\n              </Button>\n            ) : null}\n          </div>\n        ) : null}\n      </CardContent>\n    </Card>\n  )\n}\n\n/**\n * The default program: every joint sweeps its own range, each a little out of\n * step with the last, which is what a pendant's demo cycle looks like.\n */\nfunction jointSweep(clock: number, limits: [number, number][]) {\n  return limits.map(([min, max], index) => {\n    const middle = (min + max) / 2\n    const swing = (max - min) * 0.36\n    return middle + Math.sin((clock + index * 0.17) * Math.PI * 2) * swing\n  })\n}\n\nexport { ArmControls, jointSweep }\n",
      "type": "registry:ui",
      "target": "@ui/arm-controls.tsx"
    }
  ],
  "categories": [
    "robotics",
    "controls"
  ],
  "type": "registry:ui"
}