{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "robot-arm",
  "title": "Arm",
  "description": "An articulated industrial arm in SVG: any number of links, eight end effectors, four paint variants, four mounts, and pointer tracking. Drawn from any of four camera angles.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/use-robot-arm.json",
    "https://robocn.dev/r/use-pointer-target.json",
    "https://robocn.dev/r/use-robot-motion.json"
  ],
  "files": [
    {
      "path": "src/components/ui/robot-arm.tsx",
      "content": "\"use client\"\n\n/**\n * robot-arm — an articulated industrial arm drawn as SVG.\n *\n * Any number of links, eight end effectors, four paint styles, four mounts.\n * The pose comes from the shared kinematics core, so the same target produces\n * the same arm here and in `robot-arm-3d`.\n */\n\nimport * as React from \"react\"\n\nimport { usePointerTarget } from \"@/hooks/use-pointer-target\"\nimport { useRobotDrag } from \"@/hooks/use-robot-motion\"\nimport {\n  useRobotArm,\n  type RobotArmPose,\n  type RobotTarget,\n} from \"@/hooks/use-robot-arm\"\nimport {\n  add2,\n  chainMinReach,\n  chainReach,\n  forwardChain2,\n  normalize2,\n  scale2,\n  sub2,\n  toDegrees,\n  type Bend,\n  type Vec2,\n} from \"@/lib/robocn/kinematics\"\nimport {\n  capsulePath,\n  circleFootprint,\n  extrudedPath,\n  labelTransform,\n  linkRole,\n  px,\n  mountTransform,\n  resolveRobotPalette,\n  resolveRobotSize,\n  robotCamera,\n  robotSurface,\n  roundedFootprint,\n  type RobotBehavior,\n  type RobotMount,\n  type RobotPalette,\n  type RobotPaletteProps,\n  type RobotSize,\n  type RobotTool,\n  type RobotVariant,\n  type RobotView,\n} from \"@/lib/robocn/style\"\nimport { cn } from \"@/lib/utils\"\n\n/* World units. Geometry is authored once in here and only ever scaled. */\nconst VIEW_WIDTH = 132\nconst VIEW_HEIGHT = 118\nconst FLOOR = 14\nconst SHOULDER_LIFT = 13\nconst DEFAULT_REACH = 64\n/** The arm is drawn in side elevation; that is the camera it defaults to. */\nconst NATIVE_VIEW: RobotView = \"profile\"\n/** Half the width of the castings across the machine, which side elevation\n *  never showed: the limbs are tubes and the shoulder is a can. */\nconst ACROSS = 3.6\n/** Half the base plate and pedestal across the machine. */\nconst PLATE_ACROSS = 13\nconst PEDESTAL_ACROSS = 8.5\n\nconst viewNames: Record<RobotView, string> = {\n  plan: \"plan view\",\n  front: \"front elevation\",\n  profile: \"side elevation\",\n  iso: \"isometric view\",\n}\n\nexport interface RobotArmProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\" | \"target\">,\n    RobotPaletteProps {\n  /** Relative link lengths, shoulder outward. Scaled to fill `reach`. */\n  links?: number[]\n  /** Total stretch in world units; the view is 132 x 118 with the base centred. */\n  reach?: number\n  /**\n   * Controlled tool-tip goal in world units, base at (0, 0). Pass a function\n   * of elapsed seconds to script a path.\n   */\n  target?: RobotTarget\n  /**\n   * Drive the arm forward instead: one angle per link, in degrees, each\n   * relative to the previous segment. Overrides `target` and `behavior`.\n   */\n  angles?: number[]\n  /** What drives the tip when `target` is null. */\n  behavior?: RobotBehavior\n  /** Which way the elbow breaks. */\n  bend?: Bend\n  tool?: RobotTool\n  /** Where the camera stands. One arm, four projections. */\n  view?: RobotView\n  /** Jaw opening, 0 closed to 1 wide. Defaults to reacting to `active`. */\n  grip?: number\n  /** Tool running: sparks, spray, a spinning blade. Defaults to \"while moving\". */\n  active?: boolean\n  variant?: RobotVariant\n  size?: RobotSize | number\n  /** Limb weight multiplier. */\n  thickness?: number\n  mount?: RobotMount\n  /** Tip travel in world units per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  /** Seconds of phase offset, so a row of arms breaks step. */\n  phase?: number\n  /** Stencilled onto the base plate. */\n  label?: string\n  showBase?: boolean\n  showCable?: boolean\n  /** Dashed arcs for the inner and outer limits of the workspace. */\n  showEnvelope?: boolean\n  /** Joint angle readouts. On by default in the blueprint variant. */\n  showAngles?: boolean\n  showGrid?: boolean\n  /** Called whenever the pose changes — handy for wiring readouts. */\n  onPose?: (pose: RobotArmPose) => void\n  /**\n   * Press and drag the frame to send the tip there; release and the arm goes\n   * back to its behaviour. This is also the only way an arm is reachable on a\n   * touch device, where there is no pointer to follow.\n   */\n  interactive?: boolean\n  /** The dragged goal in world units, and null on release. */\n  onTargetChange?: (target: Vec2 | null) => void\n}\n\nfunction RobotArm({\n  links = [1, 0.82, 0.34],\n  reach = DEFAULT_REACH,\n  target = null,\n  angles,\n  behavior = \"idle\",\n  bend = \"up\",\n  tool = \"gripper\",\n  view = NATIVE_VIEW,\n  grip,\n  active,\n  variant = \"solid\",\n  size = \"md\",\n  thickness = 1,\n  mount = \"floor\",\n  speed,\n  animate = true,\n  paused = false,\n  phase = 0,\n  label,\n  showBase = true,\n  showCable = true,\n  showEnvelope = false,\n  showAngles,\n  showGrid,\n  onPose,\n  interactive = false,\n  onTargetChange,\n  color,\n  accent,\n  metal,\n  dark,\n  glow,\n  grid,\n  palette: paletteOverride,\n  className,\n  style,\n  ...props\n}: RobotArmProps) {\n  const uid = React.useId().replace(/[:]/g, \"\")\n  const palette = resolveRobotPalette({\n    color,\n    accent,\n    metal,\n    dark,\n    glow,\n    grid,\n    palette: paletteOverride,\n  })\n  const width = resolveRobotSize(size)\n  const height = (width * VIEW_HEIGHT) / VIEW_WIDTH\n  const blueprint = variant === \"blueprint\"\n  const annotate = showAngles ?? blueprint\n  const gridded = showGrid ?? blueprint\n\n  // Link ratios are scaled to the requested reach, so `links` stays a shape\n  // description and `reach` stays the only length anyone has to think about.\n  const scaled = React.useMemo(() => {\n    const total = links.reduce((sum, link) => sum + Math.max(link, 0.01), 0)\n    return links.map((link) => (Math.max(link, 0.01) / total) * reach)\n  }, [links, reach])\n\n  const root = React.useMemo<Vec2>(() => ({ x: 0, y: SHOULDER_LIFT }), [])\n\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const toWorld = React.useCallback((unit: Vec2) => toWorldUnits(unit, mount), [mount])\n  const pointer = usePointerTarget(svgRef, {\n    enabled: behavior === \"pointer\" && !paused,\n    toWorld,\n  })\n\n  // A press beats both the behaviour and the hover: whoever is touching the\n  // arm is driving it.\n  const [held, setHeld] = React.useState<Vec2 | null>(null)\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive && !angles,\n    onDrag: React.useCallback((unit) => {\n      const to = toWorld(unit)\n      setHeld(to)\n      onTargetChange?.(to)\n    }, [toWorld, onTargetChange, setHeld]),\n    onDragEnd: React.useCallback(() => {\n      setHeld(null)\n      onTargetChange?.(null)\n    }, [onTargetChange, setHeld]),\n  })\n\n  const pose = useRobotArm({\n    links: scaled,\n    root,\n    target: held ?? (behavior === \"pointer\" ? (target ?? pointer.target) : target),\n    behavior,\n    bend,\n    speed,\n    animate,\n    paused,\n    phase,\n  })\n\n  React.useEffect(() => {\n    onPose?.(pose)\n  }, [onPose, pose])\n\n  // Forward kinematics wins when angles are supplied: the arm is then driven\n  // joint by joint, which is what a control panel wants.\n  const driven = React.useMemo(\n    () => (angles ? forwardChain2(root, angles, scaled) : null),\n    [angles, root, scaled],\n  )\n  const engaged = active ?? (driven ? false : pose.moving)\n  const joints = driven ?? pose.joints\n  const tip = joints[joints.length - 1]\n  const wrist = joints[joints.length - 2] ?? root\n  const heading = Math.atan2(tip.y - wrist.y, tip.x - wrist.x)\n  const weight = thickness * (reach / DEFAULT_REACH)\n  const rootWeight = 3.1 * weight\n  const tipWeight = 1.9 * weight\n\n  // One arm, four cameras. The drawing is a section through the machine's own\n  // vertical plane, so it goes through `wall` and comes out untouched in side\n  // elevation; the castings around it are solids that only read off-axis.\n  const camera = robotCamera(view)\n  const offAxis = view !== NATIVE_VIEW\n  const sagittal = camera.wall(0, 90, true)\n  /** A point in the drawing, `across` units out from the centre plane. */\n  const at = (p: Vec2, across = 0) => {\n    const point = camera.project(across, p.y, -p.x)\n    return { x: point.x, y: -point.y }\n  }\n  const limbRadius = (index: number) =>\n    rootWeight + ((tipWeight - rootWeight) * index) / Math.max(1, scaled.length - 1)\n  /** A cylinder lying across the machine: the hull of its two end circles. */\n  const can = (p: Vec2, radius: number, half = ACROSS) =>\n    capsulePath(at(p, -half), at(p, half), radius)\n\n  return (\n    <svg\n      ref={svgRef}\n      role=\"img\"\n      aria-label={`Robotic arm with ${scaled.length} links and a ${tool} end effector, ${viewNames[view] ?? viewNames.profile}`}\n      viewBox={`0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`}\n      width={width}\n      height={height}\n      className={cn(\n        \"select-none overflow-hidden\",\n        interactive && !angles && \"cursor-grab touch-none\",\n        dragging && \"cursor-grabbing\",\n        className,\n      )}\n      style={{ color: palette.foreground, ...style }}\n      {...props}\n    >\n      <defs>\n        <filter id={`${uid}-glow`} x=\"-80%\" y=\"-80%\" width=\"260%\" height=\"260%\">\n          <feGaussianBlur stdDeviation=\"1.6\" result=\"blur\" />\n          <feMerge>\n            <feMergeNode in=\"blur\" />\n            <feMergeNode in=\"SourceGraphic\" />\n          </feMerge>\n        </filter>\n        <pattern\n          id={`${uid}-grid`}\n          width=\"8\"\n          height=\"8\"\n          patternUnits=\"userSpaceOnUse\"\n        >\n          <path\n            d=\"M 8 0 L 0 0 0 8\"\n            fill=\"none\"\n            stroke={palette.grid}\n            strokeWidth=\"0.3\"\n            opacity=\"0.35\"\n          />\n        </pattern>\n      </defs>\n\n      {gridded ? (\n        <rect\n          width={VIEW_WIDTH}\n          height={VIEW_HEIGHT}\n          fill={`url(#${uid}-grid)`}\n        />\n      ) : null}\n\n      {offAxis ? (\n        <g\n          data-solids\n          transform={mountTransform(mount, VIEW_WIDTH, VIEW_HEIGHT, FLOOR)}\n        >\n          {showBase ? (\n            <>\n              <path\n                d={extrudedPath(roundedFootprint(PLATE_ACROSS, 17, 4, 5), camera, 3.5, -2, 0, true)}\n                {...robotSurface(\"dark\", variant, palette, weight)}\n              />\n              <path\n                d={extrudedPath(roundedFootprint(PEDESTAL_ACROSS, 10, 3, 4), camera, SHOULDER_LIFT, 3, 0, true)}\n                {...robotSurface(\"shell\", variant, palette, weight)}\n              />\n            </>\n          ) : null}\n          {scaled.map((_, index) => (\n            <path\n              key={index}\n              d={capsulePath(at(joints[index]), at(joints[index + 1]), px(limbRadius(index)))}\n              {...robotSurface(linkRole(index), variant, palette, weight)}\n            />\n          ))}\n          {joints.map((joint, index) => (\n            <path\n              key={index}\n              d={can(joint, px((rootWeight + 1.2) * (index ? 1 - (index - 1) * 0.12 : 1.2)))}\n              {...robotSurface(\"dark\", variant, palette, weight)}\n            />\n          ))}\n          <path\n            d={extrudedPath(circleFootprint(0, -tip.x, 2.6 * weight, 10), camera, tip.y + 2.2 * weight, tip.y - 2.2 * weight, 0, true)}\n            {...robotSurface(\"metal\", variant, palette, weight)}\n          />\n        </g>\n      ) : null}\n\n      <g\n        data-view={view}\n        transform={[mountTransform(mount, VIEW_WIDTH, VIEW_HEIGHT, FLOOR), sagittal]\n          .filter(Boolean)\n          .join(\" \")}\n      >\n        {showEnvelope ? (\n          <Envelope root={root} links={scaled} palette={palette} />\n        ) : null}\n\n        {showBase ? (\n          <Base\n            palette={palette}\n            variant={variant}\n            weight={weight}\n            label={label}\n            mount={mount}\n            accentLit={engaged}\n          />\n        ) : null}\n\n        {showCable ? (\n          <Cable joints={joints} palette={palette} weight={weight} />\n        ) : null}\n\n        {scaled.map((_, index) => (\n          <Limb\n            key={index}\n            a={joints[index]}\n            b={joints[index + 1]}\n            radius={\n              rootWeight +\n              ((tipWeight - rootWeight) * index) / Math.max(1, scaled.length - 1)\n            }\n            role={linkRole(index)}\n            variant={variant}\n            palette={palette}\n            weight={weight}\n          />\n        ))}\n\n        {joints.slice(1, -1).map((joint, index) => (\n          <Joint\n            key={index}\n            at={joint}\n            radius={(rootWeight + 0.9) * (1 - index * 0.12)}\n            variant={variant}\n            palette={palette}\n            weight={weight}\n          />\n        ))}\n\n        <Joint\n          at={joints[0]}\n          radius={rootWeight + 1.8}\n          variant={variant}\n          palette={palette}\n          weight={weight}\n          shoulder\n        />\n\n        <g\n          transform={`translate(${px(tip.x)} ${px(tip.y)}) rotate(${px(toDegrees(heading))})`}\n        >\n          <ToolHead\n            tool={tool}\n            palette={palette}\n            variant={variant}\n            weight={weight}\n            grip={grip ?? (engaged ? 0.18 : 0.7)}\n            engaged={engaged}\n            glowId={`${uid}-glow`}\n          />\n        </g>\n\n        {annotate ? (\n          <Annotations\n            joints={joints}\n            angles={angles ?? pose.angles}\n            palette={palette}\n            mount={mount}\n            weight={weight}\n          />\n        ) : null}\n      </g>\n    </svg>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* parts                                                                       */\n/* -------------------------------------------------------------------------- */\n\nfunction Limb({\n  a,\n  b,\n  radius,\n  role,\n  variant,\n  palette,\n  weight,\n}: {\n  a: Vec2\n  b: Vec2\n  radius: number\n  role: ReturnType<typeof linkRole>\n  variant: RobotVariant\n  palette: RobotPalette\n  weight: number\n}) {\n  const surface = robotSurface(role, variant, palette, weight)\n  const inner = normalize2(sub2(b, a), { x: 1, y: 0 })\n  const shrink = scale2(inner, radius * 0.55)\n  return (\n    <g>\n      <path d={capsulePath(a, b, radius)} {...surface} />\n      {variant === \"solid\" ? (\n        // A thin lighter core reads as a machined channel down the limb.\n        <path\n          d={capsulePath(add2(a, shrink), sub2(b, shrink), radius * 0.34)}\n          fill={role === \"shell\" ? palette.metal : palette.dark}\n          opacity={0.55}\n        />\n      ) : null}\n    </g>\n  )\n}\n\nfunction Joint({\n  at,\n  radius,\n  variant,\n  palette,\n  weight,\n  shoulder = false,\n}: {\n  at: Vec2\n  radius: number\n  variant: RobotVariant\n  palette: RobotPalette\n  weight: number\n  shoulder?: boolean\n}) {\n  const surface = robotSurface(\"dark\", variant, palette, weight)\n  return (\n    <g transform={`translate(${px(at.x)} ${px(at.y)})`}>\n      <circle r={px(radius)} {...surface} stroke={palette.dark} strokeWidth={0.5 * weight} />\n      <circle\n        r={px(radius * 0.45)}\n        fill={variant === \"solid\" ? palette.metal : \"none\"}\n        stroke={palette.metal}\n        strokeWidth={0.5 * weight}\n        opacity={0.9}\n      />\n      {shoulder ? (\n        <circle r={px(radius * 0.16)} fill={palette.metal} opacity={0.8} />\n      ) : null}\n    </g>\n  )\n}\n\nfunction Base({\n  palette,\n  variant,\n  weight,\n  label,\n  mount,\n  accentLit,\n}: {\n  palette: RobotPalette\n  variant: RobotVariant\n  weight: number\n  label?: string\n  mount: RobotMount\n  accentLit: boolean\n}) {\n  const dark = robotSurface(\"dark\", variant, palette, weight)\n  const shell = robotSurface(\"shell\", variant, palette, weight)\n  return (\n    <g>\n      <rect\n        x={-17}\n        y={-2}\n        width={34}\n        height={5.5}\n        rx={2}\n        {...dark}\n        stroke={palette.dark}\n        strokeWidth={0.6 * weight}\n      />\n      {[-13.5, -7, 7, 13.5].map((x) => (\n        <circle key={x} cx={x} cy={0.8} r={1.1} fill={palette.metal} opacity={0.85} />\n      ))}\n      <path\n        d={`M -11 3 L 11 3 L 8 ${SHOULDER_LIFT} L -8 ${SHOULDER_LIFT} Z`}\n        {...shell}\n      />\n      <rect\n        x={-8.5}\n        y={SHOULDER_LIFT - 3.4}\n        width={17}\n        height={2.2}\n        rx={1}\n        fill={palette.accent}\n        opacity={accentLit ? 1 : 0.55}\n      />\n      {label ? (\n        <g transform={`translate(0 6.5) ${labelTransform(mount)}`}>\n          <text\n            textAnchor=\"middle\"\n            fontSize={3.6}\n            fontFamily=\"ui-monospace, monospace\"\n            letterSpacing=\"0.4\"\n            fill={palette.metal}\n          >\n            {label}\n          </text>\n        </g>\n      ) : null}\n    </g>\n  )\n}\n\n/** Loom running from the base to the wrist, sagging under its own weight. */\nfunction Cable({\n  joints,\n  palette,\n  weight,\n}: {\n  joints: Vec2[]\n  palette: RobotPalette\n  weight: number\n}) {\n  const start = { x: -6, y: SHOULDER_LIFT - 1 }\n  const end = joints[joints.length - 2] ?? joints[0]\n  const mid = joints[Math.max(1, Math.floor(joints.length / 2))]\n  return (\n    <path\n      d={`M ${px(start.x)} ${px(start.y)} Q ${px(mid.x - 5)} ${px(mid.y - 8)} ${px(end.x)} ${px(end.y)}`}\n      fill=\"none\"\n      stroke={palette.dark}\n      strokeWidth={1.1 * weight}\n      strokeLinecap=\"round\"\n      opacity={0.75}\n    />\n  )\n}\n\nfunction Envelope({\n  root,\n  links,\n  palette,\n}: {\n  root: Vec2\n  links: number[]\n  palette: RobotPalette\n}) {\n  const outer = chainReach(links)\n  const inner = chainMinReach(links)\n  return (\n    <g\n      fill=\"none\"\n      stroke={palette.grid}\n      strokeWidth={0.4}\n      strokeDasharray=\"2 2\"\n      opacity={0.7}\n    >\n      <circle cx={px(root.x)} cy={px(root.y)} r={px(outer)} />\n      {inner > 0.5 ? <circle cx={px(root.x)} cy={px(root.y)} r={px(inner)} /> : null}\n    </g>\n  )\n}\n\n/**\n * End effectors. Drawn in the wrist frame: +x runs out along the last link, so\n * a tool never has to know how the arm got there.\n */\nfunction ToolHead({\n  tool,\n  palette,\n  variant,\n  weight,\n  grip,\n  engaged,\n  glowId,\n}: {\n  tool: RobotTool\n  palette: RobotPalette\n  variant: RobotVariant\n  weight: number\n  grip: number\n  engaged: boolean\n  glowId: string\n}) {\n  const metal = robotSurface(\"metal\", variant, palette, weight)\n  const dark = robotSurface(\"dark\", variant, palette, weight)\n  const jaw = 1.6 + grip * 3.4\n\n  return (\n    <g>\n      <rect x={-1.4} y={-3} width={4.4} height={6} rx={1.2} {...dark} />\n      <rect x={2.4} y={-2.2} width={1.6} height={4.4} rx={0.6} {...metal} />\n\n      {tool === \"gripper\" ? (\n        <g>\n          {[1, -1].map((side) => (\n            <path\n              key={side}\n              d={`M 4 ${side * 1.2} L ${7.4} ${side * jaw} L ${9.6} ${side * (jaw - 0.5)} L ${9.6} ${side * (jaw + 1.1)} L ${6.6} ${side * (jaw + 1.4)} L 4 ${side * 2.6} Z`}\n              {...metal}\n            />\n          ))}\n        </g>\n      ) : null}\n\n      {tool === \"welder\" ? (\n        <g>\n          <path d=\"M 4 -1.6 L 9 -0.5 L 9 0.5 L 4 1.6 Z\" {...metal} />\n          {engaged ? (\n            <g filter={`url(#${glowId})`}>\n              <circle cx={10.2} cy={0} r={1.5} fill={palette.glow} />\n              {[0, 1, 2, 3].map((i) => (\n                <circle\n                  key={i}\n                  className=\"robocn-spark\"\n                  cx={10}\n                  cy={(i - 1.5) * 0.9}\n                  r={0.5}\n                  fill={palette.glow}\n                  style={{ animationDelay: `${i * -0.17}s` }}\n                />\n              ))}\n            </g>\n          ) : null}\n        </g>\n      ) : null}\n\n      {tool === \"painter\" ? (\n        <g>\n          <path d=\"M 4 -1.9 L 8.6 -0.8 L 8.6 0.8 L 4 1.9 Z\" {...metal} />\n          <rect x={0.4} y={-4.6} width={2.6} height={2.4} rx={0.8} {...metal} />\n          {engaged ? (\n            <g>\n              <path\n                d=\"M 8.8 -0.7 L 15 -3.4 L 15 3.4 L 8.8 0.7 Z\"\n                fill={palette.accent}\n                opacity={0.16}\n              />\n              {[0, 1, 2, 3, 4, 5].map((i) => (\n                <circle\n                  key={i}\n                  className=\"robocn-spray\"\n                  cx={9}\n                  cy={(i % 3) - 1}\n                  r={0.45}\n                  fill={palette.accent}\n                  style={{ animationDelay: `${i * -0.14}s` }}\n                />\n              ))}\n            </g>\n          ) : null}\n        </g>\n      ) : null}\n\n      {tool === \"cutter\" ? (\n        <g transform=\"translate(7.4 0)\">\n          <g\n            className={engaged ? \"robocn-spin\" : undefined}\n            style={{ transformOrigin: \"0px 0px\" }}\n          >\n            <circle r={3.4} {...metal} />\n            <circle\n              r={3.4}\n              fill=\"none\"\n              stroke={palette.dark}\n              strokeWidth={0.8}\n              strokeDasharray=\"1.4 1.4\"\n            />\n          </g>\n          <circle r={0.9} fill={palette.dark} />\n        </g>\n      ) : null}\n\n      {tool === \"scanner\" ? (\n        <g>\n          <circle cx={6.4} cy={0} r={2.6} fill=\"none\" stroke={palette.accent} strokeWidth={0.9} />\n          {engaged ? (\n            <g className=\"robocn-scan\" style={{ transformOrigin: \"6.4px 0px\" }}>\n              <path\n                d=\"M 6.4 0 L 14 -4 L 14 4 Z\"\n                fill={palette.accent}\n                opacity={0.18}\n              />\n            </g>\n          ) : null}\n        </g>\n      ) : null}\n\n      {tool === \"vacuum\" ? (\n        <g>\n          <rect x={4} y={-1.4} width={2.4} height={2.8} rx={0.6} {...metal} />\n          <path d=\"M 6.4 -3.4 L 9.8 -2 L 9.8 2 L 6.4 3.4 Z\" {...dark} />\n          <ellipse cx={10} cy={0} rx={0.9} ry={3.2} fill={palette.dark} opacity={0.9} />\n        </g>\n      ) : null}\n\n      {tool === \"magnet\" ? (\n        <g>\n          <path\n            d=\"M 4.2 -3.4 A 4 4 0 0 1 4.2 3.4 L 4.2 1.4 A 2 2 0 0 0 4.2 -1.4 Z\"\n            {...metal}\n          />\n          <rect x={3.4} y={-3.6} width={1.4} height={2.2} fill={palette.accent} />\n          <rect x={3.4} y={1.4} width={1.4} height={2.2} fill={palette.dark} />\n        </g>\n      ) : null}\n\n      <circle\n        cx={-1.8}\n        cy={0}\n        r={engaged ? 1.15 : 0.85}\n        fill={palette.glow}\n        className={engaged ? \"robocn-pulse\" : undefined}\n        filter={engaged ? `url(#${glowId})` : undefined}\n      />\n    </g>\n  )\n}\n\nfunction Annotations({\n  joints,\n  angles,\n  palette,\n  mount,\n  weight,\n}: {\n  joints: Vec2[]\n  angles: number[]\n  palette: RobotPalette\n  mount: RobotMount\n  weight: number\n}) {\n  const tip = joints[joints.length - 1]\n  return (\n    <g>\n      {joints.slice(0, -1).map((joint, index) => (\n        <g\n          key={index}\n          transform={`translate(${px(joint.x + 5)} ${px(joint.y + 5)}) ${labelTransform(mount)}`}\n        >\n          <text\n            fontSize={3.4}\n            fontFamily=\"ui-monospace, monospace\"\n            fill={palette.grid}\n          >\n            {`J${index + 1} ${angles[index]?.toFixed(0) ?? 0}°`}\n          </text>\n        </g>\n      ))}\n      <g\n        stroke={palette.accent}\n        strokeWidth={0.4 * weight}\n        opacity={0.8}\n        fill=\"none\"\n      >\n        <path d={`M ${px(tip.x - 5)} ${px(tip.y)} H ${px(tip.x + 5)}`} />\n        <path d={`M ${px(tip.x)} ${px(tip.y - 5)} V ${px(tip.y + 5)}`} />\n        <circle cx={px(tip.x)} cy={px(tip.y)} r={2.6} strokeDasharray=\"1.2 1.2\" />\n      </g>\n      <g transform={`translate(${px(tip.x + 6)} ${px(tip.y - 6)}) ${labelTransform(mount)}`}>\n        <text\n          fontSize={3.2}\n          fontFamily=\"ui-monospace, monospace\"\n          fill={palette.accent}\n        >\n          {`${tip.x.toFixed(0)}, ${tip.y.toFixed(0)}`}\n        </text>\n      </g>\n    </g>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* pointer mapping                                                             */\n/* -------------------------------------------------------------------------- */\n\n/** Inverse of {@link mountTransform}: a point in the box, in robot coordinates. */\nfunction toWorldUnits(unit: Vec2, mount: RobotMount): Vec2 {\n  const sx = unit.x * VIEW_WIDTH\n  const sy = unit.y * VIEW_HEIGHT\n  switch (mount) {\n    case \"ceiling\":\n      return { x: sx - VIEW_WIDTH / 2, y: sy - FLOOR }\n    case \"wall-left\":\n      return { x: VIEW_HEIGHT / 2 - sy, y: sx - FLOOR }\n    case \"wall-right\":\n      return { x: VIEW_HEIGHT / 2 - sy, y: VIEW_WIDTH - FLOOR - sx }\n    default:\n      return { x: sx - VIEW_WIDTH / 2, y: VIEW_HEIGHT - FLOOR - sy }\n  }\n}\n\nexport {\n  RobotArm,\n  FLOOR as ROBOT_ARM_FLOOR,\n  VIEW_HEIGHT as ROBOT_ARM_VIEW_HEIGHT,\n  VIEW_WIDTH as ROBOT_ARM_VIEW_WIDTH,\n}\n",
      "type": "registry:ui",
      "target": "@ui/robot-arm.tsx"
    }
  ],
  "categories": [
    "robotics",
    "arms"
  ],
  "type": "registry:ui"
}