{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "astromech-droid",
  "title": "Astromech droid",
  "description": "A barrel repair unit with tripod and bipod ride heights, a rotating dome and radar eye, an opening service panel, a rising periscope, and a holographic projection cone. 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/astromech-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 AstromechLegMode = \"tripod\" | \"bipod\"\nexport type AstromechTool = \"none\" | \"probe\" | \"welder\" | \"periscope\"\nexport type AstromechDome = \"round\" | \"flat\" | \"faceted\"\nexport type AstromechLivery = \"plain\" | \"banded\" | \"paneled\"\nexport type AstromechFeet = \"skid\" | \"wheel\" | \"tread\"\nexport type AstromechAntenna = \"none\" | \"whip\" | \"dish\"\nexport type AstromechSignal = \"idle\" | \"ready\" | \"warning\"\nexport type AstromechBehavior = \"work\" | \"roam\" | \"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 = 90\nconst GROUND = 192\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 AstromechDroidProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  size?: RobotSize | number\n  variant?: RobotVariant\n  /** Tripod drops the centre foot and squats; bipod retracts it and stands tall. */\n  /** Where the camera stands. One droid, four projections. */\n  view?: RobotView\n  legMode?: AstromechLegMode\n  /** Upper sensor-shell silhouette. */\n  dome?: AstromechDome\n  /** How much detailing the body carries. */\n  livery?: AstromechLivery\n  /** What the legs run on. */\n  feet?: AstromechFeet\n  /** Communications hardware on the dome. */\n  antenna?: AstromechAntenna\n  /** Service hatches down the body front, clamped to 1–3. */\n  ports?: number\n  /** Dome heading in degrees, wrapped to −180..180. Omit and `behavior` turns it. */\n  domeAngle?: number\n  /** Whole-body lean in degrees, clamped to −14..14. Omit and `behavior` rocks it. */\n  lean?: number\n  /** Open the front service panel. Omit and `behavior` works it. */\n  panel?: boolean\n  /** Instrument extended from the open panel, or raised from the dome for `periscope`. */\n  tool?: AstromechTool\n  /** Holographic projection strength, 0–1. Zero hides the cone entirely. Omit and `behavior` fades it. */\n  holo?: number\n  /** What the unit gets on with when nothing is driving it. */\n  behavior?: AstromechBehavior\n  /** Work cycles per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  phase?: number\n  /** The dome turns to whoever is pointing at it. */\n  interactive?: boolean\n  signal?: AstromechSignal\n  showGround?: boolean\n  label?: string\n}\n\nfunction AstromechDroid({\n  size = \"md\",\n  variant = \"solid\",\n  view = NATIVE_VIEW,\n  legMode = \"tripod\",\n  dome = \"round\",\n  livery = \"banded\",\n  feet = \"skid\",\n  antenna = \"none\",\n  ports = 2,\n  domeAngle,\n  lean,\n  panel,\n  tool = \"none\",\n  holo,\n  behavior = \"work\",\n  speed = 0.25,\n  animate = true,\n  paused = false,\n  phase = 0,\n  interactive = 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  ...props\n}: AstromechDroidProps) {\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  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 = astromechDroidPose(behavior, clock)\n  const aimed = pointer.target ? clamp(pointer.target.x, -1, 1) * 120 : null\n  const heading = wrap180(domeAngle ?? aimed ?? scripted.dome)\n  const tilt = finiteClamp(lean ?? scripted.lean, -14, 14)\n  const projection = finiteClamp(holo ?? scripted.holo, 0, 1)\n  const open = panel ?? scripted.panel\n  const hatches = Math.round(finiteClamp(ports, 1, 3))\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  const swing = Math.sin(toRadians(heading))\n  const depth = Math.cos(toRadians(heading))\n  // The centre foot carries the body when it is down, so ride height is a\n  // function of the chassis mode rather than a separate prop.\n  const ride = legMode === \"tripod\" ? -6 : 0\n  const deployed = open && tool !== \"none\" && tool !== \"periscope\"\n  // The drawing is a front elevation, so it goes through `wall` where the\n  // droid stands and comes out untouched straight on. The barrel is a drum\n  // rather than a panel, the centre foot is behind the other two rather than\n  // between them, and the dome sits on top of both: none of that is in one view.\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={`Astromech droid, ${legMode} chassis, ${dome} dome, ${tool} instrument, ${viewNames[view] ?? viewNames.front}`}\n      viewBox=\"0 0 180 220\"\n      width={width}\n      height={px(width * 1.22)}\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 90 10 V 204 M 18 196 H 162\" strokeDasharray=\"2 3\" />\n          <path d=\"M 38 26 H 142 V 194 H 38 Z\" strokeDasharray=\"2 3\" />\n        </g>\n      )}\n      {showGround && <ellipse cx={90} cy={195} rx={46} ry={5.5} fill={palette.dark} opacity={0.14} />}\n\n      {projection > 0 && (\n        <g data-holo opacity={px(0.2 + projection * 0.55)}>\n          <path\n            d={`M 118 ${px(120)} L ${px(150 + projection * 8)} ${px(70 - projection * 26)} L ${px(120 - projection * 6)} ${px(58 - projection * 30)} Z`}\n            fill={palette.glow}\n            opacity={0.35}\n          />\n          <path\n            d={`M 124 ${px(104)} H ${px(146 + projection * 6)}`}\n            stroke={palette.accent}\n            strokeWidth={1.2}\n            strokeDasharray=\"3 3\"\n            fill=\"none\"\n          />\n        </g>\n      )}\n\n      {offAxis && <g data-solids transform={`translate(${CENTRE} ${GROUND})`}>\n        {legMode === \"tripod\" && (\n          <path d={capsulePath(at(0, -58, -22), at(0, -8, -30), 7)} {...machined} />\n        )}\n        {([-1, 1] as const).map(side => (\n          <g key={side} data-leg={side < 0 ? \"left\" : \"right\"}>\n            <path d={capsulePath(at(side * 30, ride - 58, 12), at(side * 30, ride - 6, 12), 8)} {...machined} />\n            <path d={capsulePath(at(side * 30, ride - 5, -6), at(side * 30, ride - 5, 20), 6)} {...cast} />\n          </g>\n        ))}\n        <path d={drum(24, 128 - ride, 40 - ride)} {...shell} />\n        <path d={drum(23, 150 - ride, 128 - ride)} {...machined} />\n      </g>}\n      <Frame {...frame}>\n      <g data-view={view} transform={`translate(90 192) rotate(${px(tilt)})`}>\n        {legMode === \"tripod\" && (\n          <g data-leg=\"centre\">\n            <rect x={-7} y={-58} width={14} height={48} rx={5} {...machined} />\n            <path d=\"M -12 -12 H 12 Q 18 -12 18 -4 H -16 Z\" {...cast} />\n            {footDetail(feet, 0.8).map((node, index) => (\n              <circle data-foot key={index} cx={node[0]} cy={-8} r={node[1]} {...machined} />\n            ))}\n          </g>\n        )}\n        {[-1, 1].map((side) => (\n          <g data-leg={side === -1 ? \"left\" : \"right\"} key={side} transform={`translate(${side * 30} ${ride})`}>\n            <path d=\"M -9 -74 H 9 V -18 H -9 Z\" {...shell} />\n            <rect x={-11} y={-80} width={22} height={12} rx={4} {...machined} />\n            <path d=\"M -13 -18 H 13 L 16 -4 H -16 Z\" {...cast} />\n            {footDetail(feet, 1).map((node, index) => (\n              <circle data-foot key={index} cx={node[0]} cy={-8} r={node[1]} {...machined} />\n            ))}\n          </g>\n        ))}\n\n        <g data-body transform={`translate(0 ${ride})`}>\n          <path d=\"M -24 -128 H 24 V -26 Q 0 -18 -24 -26 Z\" {...shell} />\n          {livery !== \"plain\" && (\n            <g data-livery={livery} stroke={palette.dark} strokeWidth={1} opacity={0.5} fill=\"none\">\n              <path d=\"M -24 -112 H 24 M -24 -44 H 24\" />\n              {livery === \"paneled\" && <path d=\"M -24 -86 H 24 M -24 -66 H 24 M -8 -112 V -44 M 8 -112 V -44\" />}\n            </g>\n          )}\n          {Array.from({ length: hatches }, (_, hatch) => (\n            <rect\n              data-port\n              key={hatch}\n              x={6}\n              y={px(-100 + hatch * 18)}\n              width={14}\n              height={14}\n              rx={3}\n              {...machined}\n            />\n          ))}\n          <g data-panel={open ? \"open\" : \"closed\"}>\n            {open ? (\n              <>\n                <path d=\"M -14 -104 H 2 V -66 H -14 Z\" {...cast} transform=\"translate(-16 0) rotate(-14 -14 -104)\" />\n                <rect x={-14} y={-104} width={16} height={38} rx={2} {...machined} />\n                <g stroke={palette.accent} strokeWidth={1.2} fill=\"none\" opacity={0.8}>\n                  <path d=\"M -11 -98 H -1 M -11 -90 H -1 M -11 -82 H -1\" />\n                </g>\n              </>\n            ) : (\n              <rect x={-14} y={-104} width={16} height={38} rx={3} {...cast} />\n            )}\n          </g>\n          <circle cx={-20} cy={-34} r={6} fill={signalColor} stroke={palette.dark} strokeWidth={0.6} />\n\n          {deployed && (\n            <g data-tool={tool} transform=\"translate(-30 -86)\">\n              <rect x={0} y={-2.5} width={16} height={5} rx={2.5} {...machined} />\n              {tool === \"probe\" && (\n                <>\n                  <rect x={-10} y={-2} width={10} height={4} rx={2} {...cast} />\n                  <circle cx={-13} r={2.6} fill={signalColor} />\n                </>\n              )}\n              {tool === \"welder\" && (\n                <>\n                  <path d=\"M 0 -4 L -12 0 L 0 4 Z\" {...cast} />\n                  <circle cx={-15} r={3.4} fill={palette.glow} opacity={0.85} />\n                </>\n              )}\n            </g>\n          )}\n        </g>\n\n        <g data-dome data-dome-shape={dome} transform={`translate(${px(swing * 4)} ${ride - 128})`}>\n          <path d={domeOutline(dome)} {...shell} />\n          <path d=\"M -25 0 H 25\" stroke={palette.dark} strokeWidth={1.2} fill=\"none\" />\n          {dome === \"round\" && (\n            <path d={`M ${px(-18 + swing * 6)} -6 A 20 17 0 0 1 ${px(18 + swing * 6)} -6`} fill=\"none\" stroke={palette.dark} strokeWidth={1} opacity={0.5} />\n          )}\n          <circle\n            data-eye\n            cx={px(swing * 15)}\n            cy={-10}\n            r={px(5 + 2 * Math.abs(depth))}\n            fill={palette.dark}\n            stroke={palette.metal}\n            strokeWidth={1}\n          />\n          <circle cx={px(swing * 15)} cy={-10} r={2.4} fill={signalColor} />\n          {antenna === \"whip\" && (\n            <path data-antenna=\"whip\" d={`M ${px(-12 + swing * 5)} -14 V -44`} stroke={palette.metal} strokeWidth={1.6} fill=\"none\" strokeLinecap=\"round\" />\n          )}\n          {antenna === \"dish\" && (\n            <g data-antenna=\"dish\" transform={`translate(${px(-13 + swing * 5)} -16)`}>\n              <path d=\"M -6 0 L 6 -6 V -18 L -6 -12 Z\" {...cast} />\n              <path d=\"M 0 -3 V -12\" stroke={palette.metal} strokeWidth={1.4} fill=\"none\" />\n            </g>\n          )}\n          {tool === \"periscope\" && (\n            <g data-tool=\"periscope\" transform={`translate(${px(swing * 6)} 0)`}>\n              <rect x={-2.5} y={-40} width={5} height={24} rx={2} {...machined} />\n              <path d=\"M -7 -46 H 7 V -38 H -7 Z\" {...cast} />\n              <circle cx={4} cy={-42} r={2} fill={signalColor} />\n            </g>\n          )}\n        </g>\n      </g>\n      </Frame>\n      {label && (\n        <text x={90} y={214} 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\nconst wrap180 = (value: number) => {\n  if (!Number.isFinite(value)) return 0\n  return px(((((value + 180) % 360) + 360) % 360) - 180)\n}\n\n/** The three dome silhouettes, all filling the same 50×23 envelope. */\nconst domeOutline = (dome: AstromechDome) => {\n  switch (dome) {\n    case \"flat\":\n      return \"M -25 0 V -13 Q -25 -18 -18 -18 H 18 Q 25 -18 25 -13 V 0 Z\"\n    case \"faceted\":\n      return \"M -25 0 L -19 -15 L -8 -23 H 8 L 19 -15 L 25 0 Z\"\n    default:\n      return \"M -25 0 A 25 23 0 0 1 25 0 Z\"\n  }\n}\n\n/** Contact hardware under one foot: [x, radius] per node. */\nconst footDetail = (feet: AstromechFeet, scale: number): [number, number][] => {\n  switch (feet) {\n    case \"wheel\":\n      return [[0, px(6 * scale)]]\n    case \"tread\":\n      return [\n        [px(-7 * scale), px(3 * scale)],\n        [0, px(3 * scale)],\n        [px(7 * scale), px(3 * scale)],\n      ]\n    default:\n      return [[0, px(4.5 * scale)]]\n  }\n}\n\n/** What each behavior is doing at `clock`. Pure, so tests can read it. */\nexport function astromechDroidPose(behavior: AstromechBehavior, clock: number) {\n  const t = Number.isFinite(clock) ? clock : 0\n  const cycle = ((t % 1) + 1) % 1\n  switch (behavior) {\n    case \"roam\":\n      return { dome: Math.sin(t * Math.PI * 2) * 150, lean: Math.sin(t * Math.PI * 4) * 6, panel: false, holo: 0 }\n    case \"idle\":\n      return { dome: Math.sin(t * Math.PI * 0.6) * 22, lean: Math.sin(t * Math.PI * 0.5) * 2, panel: false, holo: 0 }\n    case \"static\":\n      return { dome: 0, lean: 0, panel: false, holo: 0 }\n    default: {\n      // Turn to the socket, open up, project, then close and look away.\n      const panel = cycle > 0.24 && cycle < 0.86\n      const holo =\n        cycle < 0.34 ? 0\n        : cycle < 0.46 ? (cycle - 0.34) / 0.12\n        : cycle < 0.72 ? 1\n        : cycle < 0.84 ? 1 - (cycle - 0.72) / 0.12\n        : 0\n      return { dome: cycle < 0.2 ? 60 : -18, lean: Math.sin(t * Math.PI * 2) * 3, panel, holo }\n    }\n  }\n}\n\nexport { AstromechDroid }\n",
      "type": "registry:ui",
      "target": "@ui/astromech-droid.tsx"
    }
  ],
  "categories": [
    "robotics",
    "droids"
  ],
  "type": "registry:ui"
}