{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cyber-trooper",
  "title": "Cyber trooper",
  "description": "A converted armoured humanoid with a blank slab head, optional side handles, three chest units, a bounded power meter, and four controlled poses. Drawn from any of four camera angles.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/use-pointer-target.json",
    "https://robocn.dev/r/use-robot-motion.json"
  ],
  "files": [
    {
      "path": "src/components/ui/cyber-trooper.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { usePointerTarget } from \"@/hooks/use-pointer-target\"\nimport { useRobotClock } from \"@/hooks/use-robot-motion\"\n\nimport { clamp, toRadians } from \"@/lib/robocn/kinematics\"\nimport {\n  aboutPoint,\n  capsulePath,\n  extrudedPath,\n  px,\n  resolveRobotPalette,\n  resolveRobotSize,\n  robotCamera,\n  robotSurface,\n  roundedFootprint,\n  type RobotPaletteProps,\n  type RobotSize,\n  type RobotVariant,\n  type RobotView,\n} from \"@/lib/robocn/style\"\nimport { cn } from \"@/lib/utils\"\n\nexport type CyberTrooperPose = \"stand\" | \"march\" | \"reach\" | \"powerdown\"\nexport type CyberTrooperChest = \"bar\" | \"vent\" | \"core\"\nexport type CyberTrooperHelmet = \"slab\" | \"domed\" | \"crested\"\nexport type CyberTrooperBuild = \"standard\" | \"heavy\"\nexport type CyberTrooperVisor = \"lamps\" | \"slit\" | \"bar\"\nexport type CyberTrooperShoulders = \"pauldron\" | \"flush\"\nexport type CyberTrooperSignal = \"idle\" | \"ready\" | \"warning\"\nexport type CyberTrooperBehavior = \"march\" | \"advance\" | \"idle\" | \"static\"\n\nexport interface CyberTrooperProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  size?: RobotSize | number\n  variant?: RobotVariant\n  /** Whole-body posture. `powerdown` slumps the frame and dims the optics. Omit and `behavior` drives it. */\n  /** Where the camera stands. One trooper, four projections. */\n  view?: RobotView\n  pose?: CyberTrooperPose\n  /** Head rotation in degrees, clamped to −40..40. Omit and it turns to whoever it is facing. */\n  headAngle?: number\n  /** What the trooper does unattended. */\n  behavior?: CyberTrooperBehavior\n  /** Steps per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  phase?: number\n  /** The head turns to the pointer, and a click cuts or restores its power. */\n  interactive?: boolean\n  onPowerChange?: (power: number) => void\n  /** Chest unit style. */\n  chestUnit?: CyberTrooperChest\n  /** Head shell silhouette. */\n  helmet?: CyberTrooperHelmet\n  /** How heavily the frame is built. Scales limb and torso width, never the height. */\n  build?: CyberTrooperBuild\n  /** Optic treatment on the faceplate. */\n  visor?: CyberTrooperVisor\n  /** Shoulder hardware: capped pauldrons, or arms flush to the torso. */\n  shoulders?: CyberTrooperShoulders\n  /** Draw the hinged jaw plate. */\n  jaw?: boolean\n  /** Normalized 0–1 power reserve shown in the chest meter. Omit and `behavior` drains it. */\n  power?: number\n  /** Draw the two side handles on the head shell. */\n  handles?: boolean\n  signal?: CyberTrooperSignal\n  showGround?: boolean\n  label?: string\n}\n\n/** The droid is drawn standing straight on; that is the camera it defaults to. */\nconst NATIVE_VIEW: RobotView = \"front\"\n/** Where it stands in the frame, and the depths a front elevation never had\n *  to give: how far the body, limbs and head reach through the picture. */\nconst CENTRE = 85\nconst GROUND = 204\nconst BODY_DEEP = 12\nconst LIMB_DEEP = 7\n\nconst viewNames: Record<RobotView, string> = {\n  plan: \"plan view\",\n  front: \"front elevation\",\n  profile: \"side elevation\",\n  iso: \"isometric view\",\n}\n\nconst poses: Record<\n  CyberTrooperPose,\n  { lean: number; leftLeg: number; rightLeg: number; leftArm: number; rightArm: number; fore: number }\n> = {\n  stand: { lean: 0, leftLeg: 0, rightLeg: 0, leftArm: 4, rightArm: -4, fore: 0 },\n  march: { lean: -4, leftLeg: 16, rightLeg: -15, leftArm: 22, rightArm: -20, fore: -12 },\n  reach: { lean: -6, leftLeg: 4, rightLeg: -3, leftArm: 12, rightArm: -102, fore: -18 },\n  powerdown: { lean: 13, leftLeg: 12, rightLeg: -10, leftArm: -6, rightArm: 6, fore: 26 },\n}\n\nfunction CyberTrooper({\n  size = \"md\",\n  variant = \"solid\",\n  view = NATIVE_VIEW,\n  pose,\n  headAngle,\n  behavior = \"march\",\n  speed = 0.4,\n  animate = true,\n  paused = false,\n  phase = 0,\n  interactive = true,\n  onPowerChange,\n  chestUnit = \"bar\",\n  helmet = \"slab\",\n  build = \"standard\",\n  visor = \"lamps\",\n  shoulders = \"pauldron\",\n  jaw = true,\n  power,\n  handles = true,\n  signal = \"ready\",\n  showGround = true,\n  label,\n  color,\n  accent,\n  metal,\n  dark,\n  glow,\n  grid,\n  palette: paletteOverride,\n  className,\n  style,\n  onPointerDown,\n  ...props\n}: CyberTrooperProps) {\n  const palette = resolveRobotPalette({ color, accent, metal, dark, glow, grid, palette: paletteOverride })\n  const width = resolveRobotSize(size)\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const clock = useRobotClock({ speed, animate: animate && behavior !== \"static\", paused, phase })\n  // A click cuts the power, and a second click restores it.\n  const [cut, setCut] = React.useState(false)\n  const pointer = usePointerTarget(svgRef, {\n    enabled: interactive && headAngle === undefined && !paused,\n    toWorld: React.useCallback((unit: { x: number; y: number }) => ({\n      x: (unit.x - 0.5) * 2,\n      y: (unit.y - 0.5) * 2,\n    }), []),\n  })\n  const scripted = cyberTrooperPose(behavior, clock)\n  const shown = pose ?? (cut ? \"powerdown\" : scripted.pose)\n  const turn = finiteClamp(\n    headAngle ?? (pointer.target ? clamp(pointer.target.x, -1, 1) * 36 : scripted.head),\n    -40,\n    40,\n  )\n  const reserve = finiteClamp(power ?? (cut ? 0 : scripted.power), 0, 1)\n  const stance = poses[shown] ?? poses.stand\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n  const down = shown === \"powerdown\"\n  const signalColor = down\n    ? palette.metal\n    : signal === \"warning\"\n      ? palette.shell\n      : signal === \"ready\"\n        ? palette.accent\n        : palette.metal\n  // The meter is a fixed track so the fill reads as a proportion, not a shape.\n  const meterWidth = px(1 + reserve * 27)\n  // Build widens the frame on one axis only, so every pose and joint still lines up.\n  const girth = build === \"heavy\" ? 1.24 : 1\n  // The drawing is a front elevation, so it goes through `wall` where the\n  // droid stands and comes out untouched straight on. A humanoid drawn from\n  // the front says nothing about its own depth: the torso, pelvis and head\n  // become boxes, and the limbs tubes set through the body.\n  const camera = robotCamera(view)\n  const offAxis = view !== NATIVE_VIEW\n  const facing = aboutPoint(camera.wall(), CENTRE, GROUND)\n  const Frame = (facing ? \"g\" : React.Fragment) as React.FC<{\n    transform?: string\n    children?: React.ReactNode\n  }>\n  const faceProps = facing ? { transform: facing } : {}\n  /** A point in the frame, leaned with the body and set `deep` toward us. */\n  const at = (x: number, y: number, deep = 0) => {\n    const tilt = toRadians(stance.lean)\n    return camera.project(\n      -(x * Math.cos(tilt) - y * Math.sin(tilt)),\n      -(x * Math.sin(tilt) + y * Math.cos(tilt)),\n      -deep,\n    )\n  }\n  /** The joints of a limb: each segment turned by its own angle, in order. */\n  const chain = (\n    ox: number,\n    oy: number,\n    deep: number,\n    segments: readonly { angle: number; length: number }[],\n  ) => {\n    let x = ox\n    let y = oy\n    const joints = [at(x, y, deep)]\n    for (const segment of segments) {\n      const a = toRadians(segment.angle)\n      x -= Math.sin(a) * segment.length\n      y += Math.cos(a) * segment.length\n      joints.push(at(x, y, deep))\n    }\n    return joints\n  }\n  /** A box between two heights above the ground, `deep` either side. */\n  const solid = (halfWidth: number, deep: number, top: number, bottom: number, x = 0) =>\n    extrudedPath(\n      roundedFootprint(halfWidth, deep, Math.min(halfWidth, deep) * 0.4, 4).map(point => ({\n        x: point.x - x,\n        y: point.y,\n      })),\n      camera, top, bottom,\n    )\n\n\n  return (\n    <svg\n      ref={svgRef}\n      role=\"img\"\n      aria-label={`Cyber trooper, ${shown} pose, ${build} build, ${chestUnit} chest unit, ${viewNames[view] ?? viewNames.front}`}\n      onPointerDown={(event) => {\n        onPointerDown?.(event)\n        if (!interactive || event.defaultPrevented) return\n        setCut((was) => {\n          onPowerChange?.(was ? scripted.power : 0)\n          return !was\n        })\n      }}\n      viewBox=\"0 0 170 236\"\n      width={width}\n      height={px(width * 1.39)}\n      className={cn(\"max-w-full select-none\", className)}\n      style={{ color: palette.foreground, ...style }}\n      {...props}\n    >\n      {variant === \"blueprint\" && (\n        <g fill=\"none\" stroke={palette.grid} strokeWidth={0.5} opacity={0.45}>\n          <path d=\"M 85 8 V 220 M 14 212 H 156\" strokeDasharray=\"2 3\" />\n          <path d=\"M 30 20 H 140 V 210 H 30 Z\" strokeDasharray=\"2 3\" />\n        </g>\n      )}\n      {showGround && <ellipse cx={85} cy={206} rx={44} ry={6} fill={palette.dark} opacity={0.14} />}\n\n      {offAxis && <g data-solids transform={`translate(${CENTRE} ${GROUND})`}>\n        {([-1, 1] as const).map(side => {\n          const [hip, knee, ankle] = chain(side * px(20 * girth), -88, side * LIMB_DEEP, [\n            { angle: side === -1 ? stance.leftLeg : stance.rightLeg, length: 40 },\n            { angle: side === -1 ? stance.leftLeg : stance.rightLeg, length: 44 },\n          ])\n          return (\n            <g key={side} data-leg={side < 0 ? \"left\" : \"right\"}>\n              <path d={capsulePath(hip, knee, px(11 * girth))} {...shell} />\n              <path d={capsulePath(knee, ankle, px(10 * girth))} {...machined} />\n            </g>\n          )\n        })}\n        {([-1, 1] as const).map(side => {\n          const [shoulder, elbow, wrist] = chain(side * px(33 * girth), -138, side * BODY_DEEP, [\n            { angle: side === -1 ? stance.leftArm : stance.rightArm, length: 45 },\n            { angle: (side === -1 ? stance.leftArm : stance.rightArm) + side * stance.fore, length: 38 },\n          ])\n          return (\n            <g key={side} data-arm={side < 0 ? \"left\" : \"right\"}>\n              <path d={capsulePath(shoulder, elbow, px(8 * girth))} {...shell} />\n              <path d={capsulePath(elbow, wrist, 6.5)} {...machined} />\n            </g>\n          )\n        })}\n        <path d={solid(px(28 * girth), BODY_DEEP, 152, 88)} {...shell} />\n        <path d={solid(px(22 * girth), BODY_DEEP - 1, 96, 80)} {...cast} />\n        <path d={solid(8, 7, 172, 150)} {...machined} />\n        <path d={solid(17, 14, 196, 166, turn * 0.16)} {...shell} />\n      </g>}\n      <Frame {...faceProps}>\n      <g data-frame data-build={build} data-view={view} transform={`translate(85 204) rotate(${px(stance.lean)})`}>\n        {[-1, 1].map((side) => {\n          const swing = side === -1 ? stance.leftLeg : stance.rightLeg\n          return (\n            <g data-leg={side === -1 ? \"left\" : \"right\"} key={side} transform={`translate(${px(side * 20 * girth)} -88) rotate(${px(swing)})`}>\n              <rect x={px(-11 * girth)} y={-4} width={px(22 * girth)} height={40} rx={5} {...shell} />\n              <rect x={px(-9 * girth)} y={36} width={px(18 * girth)} height={8} rx={3} {...cast} />\n              <rect x={px(-10 * girth)} y={44} width={px(20 * girth)} height={36} rx={4} {...machined} />\n              <path d=\"M -12 80 H 14 Q 22 80 22 88 H -13 Z\" {...cast} />\n              <path d=\"M -11 10 H 11 M -11 22 H 11\" stroke={palette.dark} strokeWidth={1} fill=\"none\" opacity={0.6} />\n            </g>\n          )\n        })}\n\n        <path\n          d={`M ${px(-31 * girth)} -146 H ${px(31 * girth)} L ${px(27 * girth)} -94 Q 0 -84 ${px(-27 * girth)} -94 Z`}\n          {...shell}\n        />\n        <path\n          d={`M ${px(-27 * girth)} -94 Q 0 -84 ${px(27 * girth)} -94 L ${px(24 * girth)} -82 Q 0 -74 ${px(-24 * girth)} -82 Z`}\n          {...cast}\n        />\n        <g data-chest={chestUnit}>\n          <rect x={-21} y={-136} width={42} height={30} rx={4} {...machined} />\n          {chestUnit === \"vent\" && (\n            <g stroke={palette.dark} strokeWidth={1.6} fill=\"none\">\n              <path d=\"M -15 -130 H 15 M -15 -123 H 15 M -15 -116 H 15\" />\n            </g>\n          )}\n          {chestUnit === \"core\" && (\n            <>\n              <circle cy={-123} r={9} {...cast} />\n              <circle cy={-123} r={px(2 + reserve * 5)} fill={signalColor} />\n            </>\n          )}\n          {chestUnit === \"bar\" && <rect x={-16} y={-128} width={32} height={12} rx={2} {...cast} />}\n          <rect x={-15} y={-103} width={30} height={6} rx={3} {...cast} />\n          <rect data-power x={-15} y={-103} width={meterWidth} height={6} rx={3} fill={signalColor} />\n        </g>\n        <path d=\"M -12 -152 H 12 V -142 H -12 Z\" {...cast} />\n\n        {[-1, 1].map((side) => {\n          const shoulder = side === -1 ? stance.leftArm : stance.rightArm\n          return (\n            <g data-arm={side === -1 ? \"left\" : \"right\"} key={side} transform={`translate(${px(side * 33 * girth)} -138) rotate(${px(shoulder)})`}>\n              {shoulders === \"pauldron\" && (\n                <rect data-pauldron x={px(-9 * girth)} y={-10} width={px(18 * girth)} height={20} rx={7} {...cast} />\n              )}\n              <rect x={px(-8 * girth)} y={8} width={px(16 * girth)} height={36} rx={5} {...shell} />\n              <g transform={`translate(0 45) rotate(${px(side * stance.fore)})`}>\n                <rect x={-7} y={0} width={14} height={36} rx={4} {...machined} />\n                <rect x={-7.5} y={36} width={15} height={12} rx={4} {...cast} />\n                <path d=\"M -4 48 V 54 M 0 48 V 55 M 4 48 V 54\" stroke={palette.metal} strokeWidth={2} strokeLinecap=\"round\" fill=\"none\" />\n              </g>\n            </g>\n          )\n        })}\n\n        <g data-head transform={`translate(${px(turn * 0.16)} -172) rotate(${px(turn * 0.09)})`}>\n          <path data-helmet={helmet} d={helmetOutline(helmet)} {...shell} />\n          <path d=\"M -14 -14 H 14 V 0 H -14 Z\" {...cast} />\n          <g data-visor={visor}>\n            {visor === \"lamps\" &&\n              [-7, 7].map((x) => (\n                <rect key={x} x={px(x - 4)} y={-11} width={8} height={7} rx={1.5} fill={signalColor} opacity={down ? 0.5 : 1} />\n              ))}\n            {visor === \"slit\" && (\n              <rect x={-12} y={-9} width={24} height={3.5} rx={1.75} fill={signalColor} opacity={down ? 0.5 : 1} />\n            )}\n            {visor === \"bar\" && (\n              <rect x={-13} y={-12} width={26} height={9} rx={2} fill={signalColor} opacity={down ? 0.5 : 1} />\n            )}\n          </g>\n          {jaw && <path data-jaw d=\"M -10 2 H 10 V 9 Q 0 14 -10 9 Z\" {...machined} />}\n          <path d=\"M -6 3 H 6 M -8 8 H 8\" stroke={palette.dark} strokeWidth={1.4} fill=\"none\" />\n          <rect x={-5} y={-30} width={10} height={9} rx={2} {...machined} />\n          {handles &&\n            [-1, 1].map((side) => (\n              <path\n                data-handle={side === -1 ? \"left\" : \"right\"}\n                key={side}\n                d={`M ${side * 16} -18 h ${side * 8} v 12 h ${-side * 8}`}\n                fill=\"none\"\n                stroke={palette.metal}\n                strokeWidth={4}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              />\n            ))}\n        </g>\n      </g>\n      </Frame>\n      {label && (\n        <text x={85} y={230} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\" fontSize={6} fill={palette.foreground}>\n          {label}\n        </text>\n      )}\n    </svg>\n  )\n}\n\nconst finiteClamp = (value: number, min: number, max: number) =>\n  Number.isFinite(value) ? clamp(value, min, max) : 0\n\n/** The three head shells, all filling the same 36×30 envelope. */\nconst helmetOutline = (helmet: CyberTrooperHelmet) => {\n  switch (helmet) {\n    case \"domed\":\n      return \"M -18 -12 Q -18 -24 0 -24 Q 18 -24 18 -12 V 8 Q 0 16 -18 8 Z\"\n    case \"crested\":\n      return \"M -18 -18 L -6 -30 H 6 L 18 -18 V 8 Q 0 16 -18 8 Z\"\n    default:\n      return \"M -18 -22 H 18 V 8 Q 0 16 -18 8 Z\"\n  }\n}\n\n/** What each behavior is doing at `clock`. Pure, so tests can read it. */\nexport function cyberTrooperPose(behavior: CyberTrooperBehavior, clock: number) {\n  const t = Number.isFinite(clock) ? clock : 0\n  const cycle = ((t % 1) + 1) % 1\n  switch (behavior) {\n    case \"advance\":\n      return { pose: cycle < 0.7 ? \"march\" : \"reach\", head: Math.sin(t * Math.PI * 2) * 22, power: 0.55 } as const\n    case \"idle\":\n      // Standing by on a slow trickle charge.\n      return { pose: \"stand\", head: Math.sin(t * Math.PI * 0.5) * 9, power: 0.5 + Math.sin(t * Math.PI) * 0.18 } as const\n    case \"static\":\n      return { pose: \"stand\", head: 0, power: 0.7 } as const\n    default:\n      // Marching draws the reserve down, then it recharges on the spot.\n      return {\n        pose: cycle < 0.82 ? \"march\" : \"stand\",\n        head: Math.sin(t * Math.PI * 2) * 14,\n        power: cycle < 0.82 ? 0.95 - cycle * 0.8 : 0.29 + (cycle - 0.82) * 3.6,\n      } as const\n  }\n}\n\nexport { CyberTrooper }\n",
      "type": "registry:ui",
      "target": "@ui/cyber-trooper.tsx"
    }
  ],
  "categories": [
    "robotics",
    "droids"
  ],
  "type": "registry:ui"
}