{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "casing-droid",
  "title": "Casing droid",
  "description": "An armoured conical casing unit with a hemisphere skirt, rotating dome, elevating eyestalk, swappable manipulator, and emitter rod. 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/casing-droid.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  circleFootprint,\n  extrudedPath,\n  px,\n  resolveRobotPalette,\n  resolveRobotSize,\n  robotCamera,\n  robotSurface,\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 CasingDroidSkirt = \"banded\" | \"smooth\" | \"ribbed\"\nexport type CasingDroidManipulator = \"none\" | \"suction\" | \"clamp\" | \"probe\"\nexport type CasingDroidDome = \"round\" | \"flat\" | \"faceted\"\nexport type CasingDroidCollar = \"slats\" | \"mesh\" | \"plain\"\nexport type CasingDroidEmitter = \"none\" | \"rod\" | \"dish\" | \"array\"\nexport type CasingDroidLamps = \"none\" | \"pair\" | \"quad\"\nexport type CasingDroidSignal = \"idle\" | \"ready\" | \"warning\"\nexport type CasingDroidBehavior = \"patrol\" | \"survey\" | \"idle\" | \"static\"\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. */\nconst CENTRE = 85\nconst GROUND = 202\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 CasingDroidProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  size?: RobotSize | number\n  variant?: RobotVariant\n  /** Lower casing treatment. Geometry of the flare is shared; the surface is not. */\n  /** Where the camera stands. One droid, four projections. */\n  view?: RobotView\n  skirt?: CasingDroidSkirt\n  /** Rows of hemispheres on the skirt, clamped to 2–4. */\n  hemisphereRows?: number\n  /** Hemispheres across each row, clamped to 3–6. */\n  hemisphereColumns?: number\n  /** Upper sensor-shell silhouette. */\n  dome?: CasingDroidDome\n  /** Mid-section treatment between the skirt and the neck. */\n  collar?: CasingDroidCollar\n  /** Rings in the neck cage, clamped to 2–5. */\n  neckRings?: number\n  /** Eyestalk length as a multiple of the standard reach, clamped to 0.5–1.8. */\n  stalkLength?: number\n  /** Dome heading in degrees, wrapped to −180..180. Swings the eyestalk around the casing. Omit and `behavior` turns it. */\n  domeAngle?: number\n  /** Eyestalk pitch in degrees, clamped to −28..28. Omit and `behavior` works it. */\n  eyeElevation?: number\n  /** What the droid does when the dome is not being driven. */\n  behavior?: CasingDroidBehavior\n  /** Sweeps per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  phase?: number\n  /** The eyestalk follows the pointer, because of course it does. */\n  interactive?: boolean\n  /** Tool on the left appendage. */\n  manipulator?: CasingDroidManipulator\n  /** Hardware on the right appendage. */\n  emitter?: CasingDroidEmitter\n  /** Dome lamp arrangement. */\n  lamps?: CasingDroidLamps\n  signal?: CasingDroidSignal\n  showGround?: boolean\n  label?: string\n}\n\nfunction CasingDroid({\n  size = \"md\",\n  variant = \"solid\",\n  view = NATIVE_VIEW,\n  skirt = \"banded\",\n  hemisphereRows = 3,\n  hemisphereColumns = 4,\n  dome = \"round\",\n  collar = \"slats\",\n  neckRings = 3,\n  stalkLength = 1,\n  domeAngle,\n  eyeElevation,\n  behavior = \"patrol\",\n  speed = 0.18,\n  animate = true,\n  paused = false,\n  phase = 0,\n  interactive = true,\n  manipulator = \"suction\",\n  emitter = \"array\",\n  lamps = \"pair\",\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  ...props\n}: CasingDroidProps) {\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  // An eyestalk that follows you is the entire point of this machine.\n  const pointer = usePointerTarget(svgRef, {\n    enabled: interactive && domeAngle === 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 = casingDroidPose(behavior, clock)\n  const watched = pointer.target\n  const heading = wrap180(domeAngle ?? (watched ? clamp(watched.x, -1, 1) * 115 : scripted.dome))\n  const pitch = finiteClamp(\n    eyeElevation ?? (watched ? clamp(-watched.y, -1, 1) * 22 : scripted.eye),\n    -28,\n    28,\n  )\n  const rows = Math.round(finiteClamp(hemisphereRows, 2, 4))\n  const columns = Math.round(finiteClamp(hemisphereColumns, 3, 6))\n  const rings = Math.round(finiteClamp(neckRings, 2, 5))\n  const reach = finiteClamp(stalkLength, 0.5, 1.8) || 1\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n  const signalColor = signal === \"warning\" ? palette.shell : signal === \"ready\" ? palette.accent : palette.metal\n\n  // The dome turns about a vertical axis, so the eyestalk swings sideways and\n  // foreshortens instead of orbiting the drawing plane.\n  const swing = Math.sin(toRadians(heading))\n  const depth = Math.cos(toRadians(heading))\n  const stalk = px((17 + 11 * depth) * reach)\n  // Studs are laid into the cone rather than onto a fixed grid, so a taller or\n  // busier skirt still keeps every hemisphere inside the silhouette.\n  const rowTop = -64\n  const rowBottom = -12\n  const rowGap = rows > 1 ? (rowBottom - rowTop) / (rows - 1) : 0\n  // The drawing is a front elevation, so it goes through `wall` where the\n  // droid stands and comes out untouched straight on. The casing is a cone of\n  // revolution and the dome a hemisphere on top of it; the manipulator and the\n  // emitter reach forward out of the shoulders rather than sideways across it.\n  const camera = robotCamera(view)\n  const offAxis = view !== NATIVE_VIEW\n  const face = aboutPoint(camera.wall(), CENTRE, GROUND)\n  const Frame = (face ? \"g\" : React.Fragment) as React.FC<{\n    transform?: string\n    children?: React.ReactNode\n  }>\n  const frame = face ? { transform: face } : {}\n  /** A point in the frame, `deep` units toward the reader. */\n  const at = (x: number, y: number, deep = 0) => camera.project(-x, -y, -deep)\n  /** A drum: round in plan, standing between two heights above the ground. */\n  const drum = (radius: number, top: number, bottom: number, x = 0) =>\n    extrudedPath(circleFootprint(-x, 0, radius, 14), camera, top, bottom)\n\n\n  return (\n    <svg\n      ref={svgRef}\n      role=\"img\"\n      aria-label={`Casing droid, ${skirt} skirt, ${dome} dome, ${manipulator} manipulator, ${viewNames[view] ?? viewNames.front}`}\n      viewBox=\"0 0 170 230\"\n      width={width}\n      height={px(width * 1.35)}\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 214 M 14 206 H 156\" strokeDasharray=\"2 3\" />\n          <path d=\"M 30 26 H 140 V 204 H 30 Z\" strokeDasharray=\"2 3\" />\n        </g>\n      )}\n      {showGround && <ellipse cx={85} cy={205} rx={48} ry={6} fill={palette.dark} opacity={0.14} />}\n\n      {offAxis && <g data-solids transform={`translate(${CENTRE} ${GROUND})`}>\n        {/* The skirt is a cone, so it is stacked bands rather than one drum. */}\n        {[0, 1, 2, 3].map(band => (\n          <path\n            key={band}\n            d={extrudedPath(\n              circleFootprint(0, 0, 42 - band * 3.8, 16),\n              camera,\n              (band + 1) * 19,\n              band * 19,\n            )}\n            {...shell}\n          />\n        ))}\n        <path d={drum(27, 104, 76)} {...machined} />\n        <path d={drum(21, 138, 104)} {...cast} />\n        <path d={drum(24, 160, 138, swing * 3)} {...shell} />\n        {([[-26, \"manipulator\"], [26, \"emitter\"]] as const).map(([x, name]) => (\n          <path key={name} d={capsulePath(at(x, -104, 6), at(x * 1.1, -104, 34), 4)} {...machined} />\n        ))}\n      </g>}\n      <Frame {...frame}>\n      <g data-view={view} transform=\"translate(85 202)\">\n        <g data-skirt={skirt}>\n          <path d=\"M -42 0 L -27 -76 H 27 L 42 0 Z\" {...shell} />\n          {skirt === \"ribbed\" && (\n            <g stroke={palette.dark} strokeWidth={1} opacity={0.55}>\n              <path d=\"M -21 -4 L -14 -74 M 0 -4 V -74 M 21 -4 L 14 -74\" />\n            </g>\n          )}\n          {skirt !== \"smooth\" &&\n            Array.from({ length: rows }, (_, row) => {\n              const y = rowTop + row * rowGap\n              // The casing tapers, so every row measures the cone at its own height.\n              const half = skirtHalfWidth(y)\n              const radius = Math.min(6.4, (half * 1.5) / columns)\n              const span = Math.max(0, half - radius - 3)\n              return (\n                <g key={row}>\n                  {Array.from({ length: columns }, (_, column) => {\n                    const slot = columns === 1 ? 0 : (column / (columns - 1)) * 2 - 1\n                    return <circle key={column} cx={px(slot * span)} cy={px(y)} r={px(radius)} {...machined} />\n                  })}\n                </g>\n              )\n            })}\n          <path d=\"M -42 -2 H 42\" stroke={palette.dark} strokeWidth={1.2} fill=\"none\" />\n        </g>\n\n        <g data-belt data-collar={collar}>\n          <path d=\"M -28 -76 H 28 L 25 -98 H -25 Z\" {...cast} />\n          {collar === \"slats\" && (\n            <g stroke={palette.metal} strokeWidth={1.2} opacity={0.7}>\n              <path d=\"M -18 -78 V -96 M -6 -78 V -96 M 6 -78 V -96 M 18 -78 V -96\" />\n            </g>\n          )}\n          {collar === \"mesh\" && (\n            <g stroke={palette.metal} strokeWidth={0.9} opacity={0.6}>\n              <path d=\"M -24 -78 L -10 -96 M -12 -78 L 2 -96 M 0 -78 L 14 -96 M 12 -78 L 24 -96\" />\n              <path d=\"M -24 -96 L -10 -78 M -12 -96 L 2 -78 M 0 -96 L 14 -78 M 12 -96 L 24 -78\" />\n            </g>\n          )}\n          <path d=\"M -27 -98 H 27 L 24 -118 H -24 Z\" {...machined} />\n          <rect data-lamp-bar x={-16} y={-116} width={32} height={7} rx={3} {...cast} />\n        </g>\n\n        <g data-neck>\n          {Array.from({ length: rings }, (_, ring) => (\n            // The cage always spans the same gap, so more rings means a finer stack.\n            <rect\n              data-neck-ring\n              key={ring}\n              x={-16}\n              y={px(-120 - (ring * 16) / (rings - 1 || 1))}\n              width={32}\n              height={5}\n              rx={2.5}\n              {...cast}\n            />\n          ))}\n          <path d=\"M -12 -120 V -138 M 0 -120 V -138 M 12 -120 V -138\" stroke={palette.metal} strokeWidth={1.4} fill=\"none\" />\n        </g>\n\n        <g data-dome data-dome-shape={dome} transform={`translate(${px(swing * 3)} -138)`}>\n          <path d={domeOutline(dome)} {...shell} />\n          <path d=\"M -25 0 H 25\" stroke={palette.dark} strokeWidth={1.2} fill=\"none\" />\n          {lampPositions(lamps).map(([x, y], index) => (\n            <circle\n              data-lamp={index}\n              key={`${x}-${y}`}\n              cx={px(x + swing * 4)}\n              cy={y}\n              r={4.2}\n              fill={signalColor}\n              stroke={palette.dark}\n              strokeWidth={0.6}\n            />\n          ))}\n          <g data-eyestalk transform={`translate(${px(swing * 16)} -6) rotate(${px(-pitch)})`}>\n            <rect x={0} y={-2} width={stalk} height={4} rx={2} {...machined} />\n            <circle cx={stalk} r={5.5} {...cast} />\n            <circle cx={stalk} r={2.6} fill={signalColor} />\n          </g>\n        </g>\n\n        <g data-manipulator={manipulator} transform=\"translate(-26 -104)\">\n          {manipulator !== \"none\" && (\n            <>\n              <circle r={5} {...cast} />\n              <rect x={-30} y={-2.5} width={30} height={5} rx={2.5} {...machined} />\n              {manipulator === \"suction\" && <path d=\"M -30 -9 L -42 -13 V 13 L -30 9 Z\" {...cast} />}\n              {manipulator === \"clamp\" && (\n                <g {...machined}>\n                  <path d=\"M -30 -3 L -43 -11 L -40 -4 L -30 -1 Z\" />\n                  <path d=\"M -30 3 L -43 11 L -40 4 L -30 1 Z\" />\n                </g>\n              )}\n              {manipulator === \"probe\" && (\n                <>\n                  <rect x={-41} y={-3.5} width={11} height={7} rx={2} {...cast} />\n                  <circle cx={-44} r={3} fill={signalColor} />\n                </>\n              )}\n            </>\n          )}\n        </g>\n\n        {emitter !== \"none\" && (\n          <g data-emitter={emitter} transform=\"translate(26 -104)\">\n            <circle r={5} {...cast} />\n            <rect x={0} y={-2} width={26} height={4} rx={2} {...machined} />\n            {emitter === \"array\" &&\n              [8, 15, 22].map((x) => (\n                <circle key={x} cx={x} r={4.5} fill=\"none\" stroke={palette.metal} strokeWidth={1.4} />\n              ))}\n            {emitter === \"dish\" && <path d=\"M 24 -11 L 34 -14 V 14 L 24 11 Z\" {...cast} />}\n            <circle cx={28} r={2.8} fill={signalColor} />\n          </g>\n        )}\n      </g>\n      </Frame>\n      {label && (\n        <text x={85} y={224} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\" fontSize={6} fill={palette.foreground}>\n          {label}\n        </text>\n      )}\n    </svg>\n  )\n}\n\n/** Half-width of the skirt at a given height: 42 at the base, 27 at the top. */\nconst skirtHalfWidth = (y: number) => 42 - (Math.min(0, Math.max(-76, y)) / -76) * 15\n\n/** The three dome silhouettes, all filling the same 50×22 envelope. */\nconst domeOutline = (dome: CasingDroidDome) => {\n  switch (dome) {\n    case \"flat\":\n      return \"M -25 0 V -12 Q -25 -17 -18 -17 H 18 Q 25 -17 25 -12 V 0 Z\"\n    case \"faceted\":\n      return \"M -25 0 L -20 -14 L -9 -22 H 9 L 20 -14 L 25 0 Z\"\n    default:\n      return \"M -25 0 A 25 22 0 0 1 25 0 Z\"\n  }\n}\n\n/** Where the dome lamps sit, in dome-local coordinates. */\nconst lampPositions = (lamps: CasingDroidLamps): [number, number][] => {\n  switch (lamps) {\n    case \"none\":\n      return []\n    case \"quad\":\n      return [\n        [-15, -9],\n        [-6, -16],\n        [6, -16],\n        [15, -9],\n      ]\n    default:\n      return [\n        [-13, -13],\n        [13, -13],\n      ]\n  }\n}\n\nconst finiteClamp = (value: number, min: number, max: number) =>\n  Number.isFinite(value) ? clamp(value, min, max) : 0\n\n/** Heading is cyclic, so out-of-range input wraps instead of sticking at a limit. */\nconst wrap180 = (value: number) => {\n  if (!Number.isFinite(value)) return 0\n  return px(((((value + 180) % 360) + 360) % 360) - 180)\n}\n\n/** The sweep each behavior traces, sampled at `clock`. Pure, so tests can read it. */\nexport function casingDroidPose(behavior: CasingDroidBehavior, clock: number) {\n  const t = Number.isFinite(clock) ? clock : 0\n  switch (behavior) {\n    case \"survey\":\n      return { dome: Math.sin(t * Math.PI * 2) * 165, eye: Math.sin(t * Math.PI * 4) * 16 }\n    case \"idle\":\n      return { dome: Math.sin(t * Math.PI * 0.6) * 14, eye: Math.sin(t * Math.PI * 0.9) * 5 }\n    case \"static\":\n      return { dome: 0, eye: 0 }\n    default: {\n      // Patrol holds a bearing, swings to the next one, and holds again.\n      const cycle = ((t % 1) + 1) % 1\n      const dome = cycle < 0.4 ? -70 : cycle < 0.5 ? -70 + (cycle - 0.4) * 1400 : cycle < 0.9 ? 70 : 70 - (cycle - 0.9) * 1400\n      return { dome, eye: Math.sin(t * Math.PI * 1.4) * 7 }\n    }\n  }\n}\n\nexport { CasingDroid }\n",
      "type": "registry:ui",
      "target": "@ui/casing-droid.tsx"
    }
  ],
  "categories": [
    "robotics",
    "droids"
  ],
  "type": "registry:ui"
}