{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "custodian-droid",
  "title": "Custodian droid",
  "description": "A floating armoured custodian: a rounded casing whose armour segments ride radial rails and bloom into a corona around a lit chassis, with a deep-set gimballed optic behind a three-arm bracket cage and a ring of voice cells. Work the shell by hand 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/custodian-droid.tsx",
      "content": "\"use client\"\n\n/**\n * custodian-droid — a floating armoured custodian.\n *\n * No limbs, no wheels, no rotor: a rounded casing hanging in the air with one\n * deep-set optic on its face. The mechanism is the casing itself. The armour is\n * a ring of segments riding radial rails over a lit chassis, and one `open`\n * number runs every one of them out into a corona — the front edge travelling\n * further than the back, so the shell blooms rather than merely dilating, and\n * the rail that carries each plate showing as a strut that lengthens.\n *\n * Everything is modelled once in world units — `x` across the face, `y` down\n * it, `z` out of it toward the room — and projected, so there is no second\n * drawing for any camera. The shell rolls with the machine's drift; the optic\n * is gimballed and stays level, which is what makes it read as floating.\n *\n * Design note: docs/custodian-droid.md.\n */\n\nimport * as React from \"react\"\n\nimport { usePointerTarget } from \"@/hooks/use-pointer-target\"\nimport { arrowStep, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport { clamp, toRadians, type Vec2 } from \"@/lib/robocn/kinematics\"\nimport {\n  capsulePath,\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 CustodianDroidBehavior = \"watch\" | \"survey\" | \"alert\" | \"static\"\n\n/** The droid is drawn straight on; that is the camera it defaults to. */\nconst NATIVE_VIEW: RobotView = \"front\"\n\nconst VIEW_WIDTH = 200\nconst VIEW_HEIGHT = 210\n/** The optical axis at rest, in view units. */\nconst ORIGIN = { x: 100, y: 94 }\n\n/* Everything below is in world units about the optical axis:\n   `x` across the face, `y` *down* it, `z` out of it toward the room. */\n\n/** The casing outline the armour segments are cut from. */\nconst CASE_HALF_W = 62\nconst CASE_HALF_H = 49\nconst CASE_RADIUS = 20\nconst CASE_FRONT = 38\nconst CASE_BACK = -42\n/** Where the armour ring starts, outside the optic recess. */\nconst R_INNER = 34\n/** Travel at the front and back edges: the difference is the bloom. */\nconst TRAVEL_FRONT = 22\nconst TRAVEL_BACK = 12\n/** Degrees of gap left between neighbouring segments when they are seated. */\nconst SEAM = 1.1\n/** The corner break: how far the outermost rim of a solid sits inside the body\n *  behind it, and how deep that chamfer runs. A casing with square rims reads\n *  as a slab from the side; this is what keeps it a rounded cube. */\nconst CHAMFER = 0.9\nconst CHAMFER_Z = 9\n\n/** The chassis the armour stands off, and the lattice lit on its face. */\nconst CORE_HALF_W = 50\nconst CORE_HALF_H = 40\nconst CORE_RADIUS = 16\nconst CORE_FRONT = 30\nconst CORE_BACK = -36\nconst LATTICE_INNER = 35\nconst LATTICE_OUTER = 47\nconst LATTICE_RIBS = 24\n\n/** The rail each segment rides, rooted inside the chassis. */\nconst RAIL_ROOT = 30\n\n/** The optic recess, and the gimballed cell inside it. */\nconst BORE = 23\nconst PIVOT_Z = CORE_FRONT - 20\nconst BEZEL_OUTER = 19\nconst BEZEL_BORE = 15\nconst BEZEL_BACK = 6\nconst BEZEL_FRONT = 16\nconst GLASS_PLANE = 17\nconst GLASS_RADIUS = 14\nconst PUPIL_PLANE = 17.6\nconst PUPIL_RADIUS = 6.5\nconst PAN_LIMIT = 18\nconst TILT_LIMIT = 13\n\n/** The ring of voice cells round the lens, inside the recess. */\nconst RING_RADIUS = 18\nconst RING_CELLS = 12\n\n/** The bracket cage: three arms off the face to a ring standing proud of it. */\nconst CAGE_ROOT = 30\nconst CAGE_RING = 15\nconst CAGE_PROUD = 16\nconst CAGE_ARMS = [90, 210, 330] as const\n/** How far the lower arm is drawn down past the shell, as a keel. */\nconst PRONG = 44\n\n/** Where the deck is, and how far the machine floats off it. */\nconst GROUND = 78\nconst RISE = 12\n\n/** Opening units per second while the shell eases back into its behaviour. */\nconst OPEN_RATE = 1.1\nconst ARC_STEPS = 7\nconst RING_STEPS = 28\n\n/** How far the camera pulls back so the machine still fits the frame. */\nconst fits: Record<RobotView, number> = { plan: 0.94, front: 1, profile: 1, iso: 0.92 }\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 CustodianDroidProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  size?: RobotSize | number\n  variant?: RobotVariant\n  /** Where the camera stands. One droid, four projections. */\n  view?: RobotView\n  /** The shell, 0 seated to 1 run all the way out. Supplying it stops the loop. */\n  open?: number\n  /** What it does when `open` is not supplied. */\n  behavior?: CustodianDroidBehavior\n  /** Cycles per second: one float, one sweep of the room. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  /** Seconds of offset, so a pair of them breaks step. */\n  phase?: number\n  /** Drag across it to work the shell, or use the arrow keys. */\n  interactive?: boolean\n  onOpenChange?: (open: number) => void\n  /** Controlled optic aim in −1..1; overrides pointer tracking. */\n  look?: Vec2 | null\n  /** The optic follows the page pointer while `look` is null. */\n  track?: boolean\n  /** Lit cells in the ring round the lens, 0..1. Omit and the behaviour works it. */\n  voice?: number\n  /** Armour segments in the shell, 4–10. */\n  plates?: number\n  signal?: \"idle\" | \"ready\" | \"warning\"\n  showGround?: boolean\n  label?: string\n}\n\nfunction CustodianDroid({\n  size = \"md\",\n  variant = \"solid\",\n  view = NATIVE_VIEW,\n  open,\n  behavior = \"watch\",\n  speed = 0.3,\n  animate = true,\n  paused = false,\n  phase = 0,\n  interactive = true,\n  onOpenChange,\n  look = null,\n  track = true,\n  voice,\n  plates = 6,\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  role,\n  tabIndex,\n  onKeyDown,\n  onBlur,\n  ...props\n}: CustodianDroidProps) {\n  const controlled = open !== undefined\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const [held, setHeld] = React.useState<number | null>(null)\n  const hold = controlled ? finiteClamp(open, 0, 1, 0.5) : held\n\n  const goal = React.useCallback(\n    (clock: number) => custodianDroidPose(behavior, clock).open,\n    [behavior],\n  )\n  const motion = useRobotScalar(goal, {\n    rate: OPEN_RATE,\n    hold,\n    speed,\n    paused,\n    phase,\n    animate: animate && !controlled && behavior !== \"static\",\n  })\n  const spread = finiteClamp(motion.value, 0, 1, 0.5)\n  const scripted = custodianDroidPose(behavior, motion.clock)\n  const lift = clamp(scripted.lift, 0, 1)\n  const roll = clamp(scripted.roll, -1, 1) * 7\n  const talk = finiteClamp(voice ?? scripted.voice, 0, 1, 0)\n\n  const apply = React.useCallback(\n    (next: number) => {\n      const bounded = round3(clamp(next, 0, 1))\n      setHeld(bounded)\n      onOpenChange?.(bounded)\n    },\n    [onOpenChange],\n  )\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    // Across the box is the whole stroke: pull it open, push it shut.\n    onDrag: React.useCallback((unit: Vec2) => apply((unit.x - 0.08) / 0.84), [apply]),\n    onDragEnd: React.useCallback(() => setHeld(null), []),\n  })\n\n  const pointer = usePointerTarget(svgRef, {\n    enabled: track && !look,\n    within: \"window\",\n    persist: true,\n    toWorld: React.useCallback(\n      (unit: Vec2) => ({\n        x: clamp((unit.x - 0.5) * 2, -1, 1),\n        y: clamp((unit.y - 0.5) * 2, -1, 1),\n      }),\n      [],\n    ),\n  })\n  const gaze = look ?? pointer.target ?? { x: 0, y: 0 }\n  const pan = clamp(finite(gaze.x), -1, 1) * PAN_LIMIT\n  const tilt = -clamp(finite(gaze.y), -1, 1) * TILT_LIMIT\n\n  const segments = Math.round(finiteClamp(plates, 4, 10, 6))\n\n  const palette = resolveRobotPalette({ color, accent, metal, dark, glow, grid, palette: paletteOverride })\n  const width = resolveRobotSize(size)\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n  const signalColor =\n    signal === \"warning\" ? palette.shell : signal === \"ready\" ? palette.accent : palette.metal\n\n  /* -------------------------------------------------------------- camera */\n\n  const camera = robotCamera(view)\n  const hover = -lift * RISE\n  /** A point on the machine, carried by the float. */\n  const at = (x: number, y: number, z = 0): Vec2 => {\n    const point = camera.project(-x, -(y + hover), -z)\n    return { x: ORIGIN.x + point.x, y: ORIGIN.y + point.y }\n  }\n  /** The same point on the deck, which does not float. */\n  const atDeck = (x: number, y: number, z = 0): Vec2 => {\n    const point = camera.project(-x, -y, -z)\n    return { x: ORIGIN.x + point.x, y: ORIGIN.y + point.y }\n  }\n  const towardCamera = (x: number, y: number, z: number) => camera.depth(-x, -(y + hover), -z)\n  /** True while there is a face to see: the front elevation and the isometric. */\n  const faceVisible = towardCamera(0, 0, 1) > towardCamera(0, 0, 0)\n\n  /**\n   * The affine map from flat artwork on a plane `z` out of the face onto the\n   * screen. Panel coordinates are the artwork's own — x across, y down — so a\n   * grid of cells or a run of grooves is drawn once and comes out skewed\n   * correctly from every camera that can see it.\n   */\n  const facePlane = (z: number) => {\n    const base = at(0, 0, z)\n    const ex = at(1, 0, z)\n    const ey = at(0, 1, z)\n    return `matrix(${px(ex.x - base.x)} ${px(ex.y - base.y)} ${px(ey.x - base.x)} ${px(ey.y - base.y)} ${px(base.x)} ${px(base.y)})`\n  }\n\n  /**\n   * A solid: a stack of cross-sections pushed out of the face, walled with the\n   * quad strip between each neighbouring pair. Stacking rims rather than\n   * extruding one is what breaks the corner back: the outermost rim of a\n   * casing sits inside the one behind it, so the block reads as a rounded cube\n   * from every camera instead of a slab with sharp rims.\n   */\n  const prism = (rims: readonly { outline: readonly Vec2[]; z: number }[]) => {\n    const rings = rims.map((rim) => rim.outline.map((p) => at(p.x, p.y, rim.z)))\n    const wall: string[] = []\n    for (let index = 0; index < rings.length - 1; index += 1) {\n      const near = rings[index]\n      const far = rings[index + 1]\n      for (let step = 0; step < near.length; step += 1) {\n        const next = (step + 1) % near.length\n        // Wound the same way every time: a camera that folds the section onto\n        // itself makes neighbouring quads overlap, and opposite windings would\n        // cancel each other out under the nonzero fill rule.\n        wall.push(facePath([near[step], near[next], far[next], far[step]]))\n      }\n    }\n    return {\n      face: polygonPath(rings[0]),\n      rear: polygonPath(rings[rings.length - 1]),\n      wall: wall.join(\" \"),\n      // Every rim as one stroke-only path: the corner break and the seams\n      // between neighbouring parts are the only thing a side elevation has.\n      rims: rings.map((ring) => polygonPath(ring)).join(\" \"),\n    }\n  }\n\n  /* --------------------------------------------------------------- shell */\n\n  const armour = Array.from({ length: segments }, (_, index) => {\n    const centre = roll + (index * 360) / segments\n    const half = 180 / segments - SEAM\n    const push = polar(centre, 1)\n    const shift = (outline: readonly Vec2[], distance: number) =>\n      outline.map((p) => ({ x: p.x + push.x * distance, y: p.y + push.y * distance }))\n    const full = wedgeOutline(centre, half, 1)\n    const broken = wedgeOutline(centre, half, CHAMFER)\n    const front = shift(full, spread * TRAVEL_FRONT)\n    const back = shift(full, spread * TRAVEL_BACK)\n    const rims = [\n      { outline: shift(broken, spread * TRAVEL_FRONT), z: CASE_FRONT },\n      { outline: front, z: CASE_FRONT - CHAMFER_Z },\n      { outline: back, z: CASE_BACK + CHAMFER_Z },\n      { outline: shift(broken, spread * TRAVEL_BACK), z: CASE_BACK },\n    ]\n    // The rail: rooted in the chassis, out to the plate it carries.\n    const travel = (spread * (TRAVEL_FRONT + TRAVEL_BACK)) / 2\n    const rail = capsulePath(\n      at(push.x * RAIL_ROOT, push.y * RAIL_ROOT, 0),\n      at(push.x * (R_INNER + travel + 3), push.y * (R_INNER + travel + 3), 0),\n      2.4,\n    )\n    // One groove run parallel to the outer edge — a fraction of the casing's\n    // own reach on each bearing, so it never runs off the plate it belongs to\n    // — and a bolt at each end of it. Two marks: armour, not quilting.\n    const seamAngle = (t: number) => centre - half * 0.66 + t * half * 1.32\n    const seamPoint = (t: number, depth: number) => {\n      const angle = seamAngle(t)\n      const radius =\n        R_INNER + (casingRadius(angle) * CHAMFER - R_INNER) * depth + spread * TRAVEL_FRONT\n      const point = polar(angle, radius)\n      return at(point.x, point.y, CASE_FRONT)\n    }\n    const grooves = [\n      polylinePath(\n        Array.from({ length: ARC_STEPS }, (_, step) => seamPoint(step / (ARC_STEPS - 1), 0.62)),\n      ),\n    ]\n    const bolts = [0.08, 0.92].map((t) => seamPoint(t, 0.24))\n    // Where the plate sits on its own bearing, at the face it presents. The\n    // shell is a ring nested round the chassis, so this is what decides which\n    // half of it the chassis is behind — and in the front elevation it puts\n    // every plate in front of the chassis, which is where they stand.\n    const middle = (R_INNER + travel + casingRadius(centre) + spread * TRAVEL_FRONT) / 2\n    return {\n      index,\n      depth: towardCamera(push.x * middle, push.y * middle, CASE_FRONT),\n      ...prism(rims),\n      rail,\n      grooves,\n      bolts,\n    }\n  })\n\n  const coreOutline = rectOutline(CORE_HALF_W, CORE_HALF_H, CORE_RADIUS)\n  const coreBroken = rectOutline(\n    CORE_HALF_W * CHAMFER,\n    CORE_HALF_H * CHAMFER,\n    CORE_RADIUS * CHAMFER,\n  )\n  const chassis = prism([\n    { outline: coreBroken, z: CORE_FRONT },\n    { outline: coreOutline, z: CORE_FRONT - CHAMFER_Z },\n    { outline: coreOutline, z: CORE_BACK + CHAMFER_Z },\n    { outline: coreBroken, z: CORE_BACK },\n  ])\n  const coreDepth = towardCamera(0, 0, CORE_FRONT)\n\n  /* --------------------------------------------------------------- optic */\n\n  // The cell frame: `axis` out of the lens, `across` horizontal in the cell,\n  // `up` completing it. `across` ignores the tilt, which is what makes the\n  // trunnion line the tilt axis rather than ornament.\n  const yaw = toRadians(pan)\n  const pitch = toRadians(tilt)\n  const axis = {\n    x: Math.cos(pitch) * Math.sin(yaw),\n    y: -Math.sin(pitch),\n    z: Math.cos(pitch) * Math.cos(yaw),\n  }\n  const across = { x: Math.cos(yaw), y: 0, z: -Math.sin(yaw) }\n  const up = {\n    x: Math.sin(pitch) * Math.sin(yaw),\n    y: -Math.cos(pitch),\n    z: Math.sin(pitch) * Math.cos(yaw),\n  }\n  /** A point in the cell: `a` across it, `b` down it, `d` along its axis. */\n  const cellAt = (a: number, b: number, d: number) =>\n    at(\n      across.x * a + up.x * b + axis.x * d,\n      across.y * a + up.y * b + axis.y * d,\n      PIVOT_Z + across.z * a + up.z * b + axis.z * d,\n    )\n  /** A disc in the cell's own plane, sampled ready to draw. */\n  const disc = (radius: number, d: number) =>\n    Array.from({ length: RING_STEPS }, (_, step) => {\n      const angle = (step / RING_STEPS) * Math.PI * 2\n      return cellAt(Math.cos(angle) * radius, Math.sin(angle) * radius, d)\n    })\n\n  const barrel = polygonPath([...disc(BEZEL_OUTER, BEZEL_BACK), ...disc(BEZEL_OUTER, BEZEL_FRONT).reverse()])\n  const bezel = polygonPath(disc(BEZEL_OUTER, BEZEL_FRONT))\n  const tube = polygonPath(disc(BEZEL_BORE, BEZEL_FRONT - 1))\n  const glass = polygonPath(disc(GLASS_RADIUS, GLASS_PLANE))\n  const pupil = polygonPath(disc(PUPIL_RADIUS, PUPIL_PLANE))\n  const spark = polygonPath(\n    Array.from({ length: 10 }, (_, step) => {\n      const angle = (step / 10) * Math.PI * 2\n      return cellAt(-3.4 + Math.cos(angle) * 2.2, -3.4 + Math.sin(angle) * 2.2, PUPIL_PLANE + 0.3)\n    }),\n  )\n\n  const cells = Array.from({ length: RING_CELLS }, (_, index) => {\n    const centre = (index * 360) / RING_CELLS - 90\n    const half = 180 / RING_CELLS - 3\n    const corners = [\n      polar(centre - half, RING_RADIUS - 2.4),\n      polar(centre + half, RING_RADIUS - 2.4),\n      polar(centre + half, RING_RADIUS + 2.4),\n      polar(centre - half, RING_RADIUS + 2.4),\n    ].map((p) => at(p.x, p.y, CORE_FRONT + 0.4))\n    // Cells light out from the top of the ring, so quiet speech is a flicker.\n    const bright = Math.abs(((index + 0.5) / RING_CELLS) - 0.5) * 2 < talk\n    return { index, path: polygonPath(corners), lit: bright }\n  })\n\n  const cage = CAGE_ARMS.map((angle, index) => {\n    const root = polar(angle, CAGE_ROOT)\n    const hub = polar(angle, CAGE_RING)\n    return {\n      index,\n      path: capsulePath(\n        at(root.x, root.y, CORE_FRONT),\n        at(hub.x, hub.y, CORE_FRONT + CAGE_PROUD),\n        2.3,\n      ),\n    }\n  })\n  const cageRing = polygonPath(\n    Array.from({ length: RING_STEPS }, (_, step) => {\n      const angle = (step / RING_STEPS) * 360\n      const point = polar(angle, CAGE_RING)\n      return at(point.x, point.y, CORE_FRONT + CAGE_PROUD)\n    }),\n  )\n  const keel = capsulePath(\n    at(0, CAGE_ROOT, CORE_FRONT),\n    at(0, PRONG, CORE_FRONT + 3),\n    2.2,\n  )\n  /** The cage stands proud of the face; that is where the assembly sits. */\n  const opticDepth = towardCamera(0, 0, CORE_FRONT + CAGE_PROUD)\n\n  /** The optic recess, cut into the chassis face, and its machined rim. */\n  const boreRing = (z: number) =>\n    Array.from({ length: RING_STEPS }, (_, step) => {\n      const point = polar((step / RING_STEPS) * 360, BORE)\n      return at(point.x, point.y, z)\n    })\n  const bore = polygonPath(boreRing(CORE_FRONT - 1))\n  const boreRim = polygonPath(boreRing(CORE_FRONT))\n  const lamp = at(0, -(BORE + 5), CORE_FRONT)\n\n  // The lattice is lit on the chassis face, out to the chassis's own edge on\n  // each bearing — it is under the armour, and the gaps are what let it out.\n  const lattice = Array.from({ length: LATTICE_RIBS }, (_, index) => {\n    const angle = (index * 360) / LATTICE_RIBS\n    const reach = Math.min(\n      LATTICE_OUTER,\n      outlineRadius(angle, CORE_HALF_W, CORE_HALF_H, CORE_RADIUS) - 4,\n      // Never past the armour: what is lit is what the segments have uncovered.\n      R_INNER + spread * TRAVEL_FRONT,\n    )\n    const inner = polar(angle, LATTICE_INNER)\n    const outer = polar(angle, reach)\n    return reach <= LATTICE_INNER + 2\n      ? \"\"\n      : polylinePath([at(inner.x, inner.y, CORE_FRONT), at(outer.x, outer.y, CORE_FRONT)])\n  })\n\n  const shadow = polygonPath(\n    Array.from({ length: RING_STEPS }, (_, step) => {\n      const angle = (step / RING_STEPS) * Math.PI * 2\n      return atDeck(Math.cos(angle) * (34 + lift * 10), GROUND, Math.sin(angle) * (34 + lift * 10))\n    }),\n  )\n\n  /* ----------------------------------------------------------- draw order */\n\n  const core = (\n    <g data-core key=\"core\">\n      <path d={chassis.rear} {...cast} />\n      <path d={chassis.wall} {...cast} />\n      <path d={chassis.rims} fill=\"none\" stroke={palette.metal} strokeWidth={0.6} opacity={0.25} />\n      <path d={chassis.face} {...cast} />\n      {faceVisible && (\n        <>\n          {/* The lattice is lit on the chassis, under the armour: the gaps\n              between the segments are what let it out. */}\n          <g data-lattice fill=\"none\" stroke={palette.glow} strokeWidth={1.1} opacity={px(0.16 + spread * 0.64)}>\n            {lattice.map((rib, index) => (\n              <path key={index} d={rib} />\n            ))}\n          </g>\n          <g transform={facePlane(CORE_FRONT + 0.2)} fill=\"none\" stroke={palette.dark} strokeWidth={0.9} opacity={0.5}>\n            <circle r={R_INNER - 2} />\n          </g>\n          <path d={bore} fill={palette.dark} opacity={0.95} />\n          <path d={boreRim} fill=\"none\" stroke={palette.metal} strokeWidth={1.6} opacity={0.8} />\n          <g data-ring>\n            {cells.map((cell) => (\n              <path\n                key={cell.index}\n                data-cell={cell.index}\n                data-lit={cell.lit ? \"\" : undefined}\n                d={cell.path}\n                fill={cell.lit ? signalColor : palette.metal}\n                opacity={cell.lit ? 1 : 0.45}\n              />\n            ))}\n          </g>\n          <circle\n            cx={px(lamp.x)}\n            cy={px(lamp.y)}\n            r={2.6}\n            fill={signalColor}\n            className={signal === \"ready\" ? \"robocn-pulse\" : undefined}\n          />\n        </>\n      )}\n    </g>\n  )\n\n  const optic = (\n    <g key=\"optic\">\n      <g data-optic>\n        <path data-barrel d={barrel} {...machined} />\n        <path d={bezel} {...machined} />\n        <path d={tube} fill={palette.dark} />\n        <path\n          data-lens\n          d={glass}\n          fill={palette.accent}\n          fillOpacity={variant === \"solid\" ? 0.55 : 0.3}\n          stroke={palette.metal}\n          strokeWidth={1}\n        />\n        <path d={pupil} fill={palette.glow} opacity={0.95} />\n        <path d={spark} fill={palette.foreground} opacity={0.35} />\n      </g>\n      <g data-cage>\n        {cage.map((arm) => (\n          <path key={arm.index} data-arm={arm.index} d={arm.path} {...machined} />\n        ))}\n        <path d={keel} {...machined} />\n        <path d={cageRing} fill=\"none\" stroke={palette.metal} strokeWidth={2.2} />\n      </g>\n    </g>\n  )\n\n  const order = [\n    ...armour.map((plate) => ({\n      depth: plate.depth,\n      node: (\n        <g data-shell key={`plate-${plate.index}`}>\n          <Plate plate={plate} shell={shell} cast={cast} machined={machined} palette={palette} variant={variant} />\n        </g>\n      ),\n    })),\n    { depth: coreDepth, node: core },\n    { depth: opticDepth, node: optic },\n  ].sort((a, b) => a.depth - b.depth)\n\n  const state = dragging\n    ? \"worked by hand\"\n    : behavior === \"static\"\n      ? \"parked\"\n      : behavior === \"alert\"\n        ? \"on alert\"\n        : behavior === \"survey\"\n          ? \"surveying\"\n          : \"watching\"\n  const readout = Math.round(spread * 100)\n  const fit = fits[view] ?? 1\n\n  return (\n    <svg\n      ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={`Custodian droid, ${state}, shell open ${readout} percent, ${viewNames[view] ?? viewNames.front}`}\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? 100 : undefined}\n      aria-valuenow={interactive ? readout : undefined}\n      aria-valuetext={interactive ? `${readout} percent open` : undefined}\n      tabIndex={tabIndex ?? (interactive ? 0 : undefined)}\n      viewBox={`0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`}\n      width={width}\n      height={px((width * VIEW_HEIGHT) / VIEW_WIDTH)}\n      className={cn(\n        \"max-w-full select-none\",\n        interactive &&\n          \"cursor-grab touch-none focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[currentColor]\",\n        dragging && \"cursor-grabbing\",\n        className,\n      )}\n      style={{ color: palette.foreground, ...style }}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (!interactive || event.defaultPrevented) return\n        const delta = arrowStep(event.key, event.shiftKey ? 0.25 : 0.1, 0.25)\n        if (delta !== 0) apply(spread + delta)\n        else if (event.key === \"Home\") apply(0)\n        else if (event.key === \"End\") apply(1)\n        else return\n        event.preventDefault()\n      }}\n      onBlur={(event) => {\n        onBlur?.(event)\n        if (!dragging) setHeld(null)\n      }}\n      {...props}\n    >\n      {variant === \"blueprint\" && (\n        <g fill=\"none\" stroke={palette.grid} strokeWidth={0.5} opacity={0.45}>\n          <path\n            d={`M 12 ${px(ORIGIN.y)} H 188 M ${ORIGIN.x} 12 V ${VIEW_HEIGHT - 14}`}\n            strokeDasharray=\"2 3\"\n          />\n          <circle\n            cx={ORIGIN.x}\n            cy={px(ORIGIN.y + hover)}\n            r={px(R_INNER + TRAVEL_FRONT + 30)}\n            strokeDasharray=\"4 3\"\n          />\n        </g>\n      )}\n\n      <g transform={fit === 1 ? undefined : `translate(${ORIGIN.x} ${ORIGIN.y}) scale(${fit}) translate(${-ORIGIN.x} ${-ORIGIN.y})`}>\n        {showGround && (\n          <path data-contact d={shadow} fill={palette.dark} opacity={px(0.2 - lift * 0.08)} />\n        )}\n\n        <g data-custodian data-view={view}>\n          {/* Painter's order: every part sorted by its own nearest corner, so\n              the armour passes in front of the chassis from one camera and\n              behind it from another without a second drawing. */}\n          {order.map((part) => part.node)}\n        </g>\n      </g>\n\n      {label && (\n        <text\n          x={ORIGIN.x}\n          y={VIEW_HEIGHT - 6}\n          textAnchor=\"middle\"\n          fontFamily=\"ui-monospace, monospace\"\n          fontSize={6}\n          fill={palette.foreground}\n        >\n          {label}\n        </text>\n      )}\n    </svg>\n  )\n}\n\n/* ------------------------------------------------------------------ parts */\n\ninterface PlateArt {\n  index: number\n  depth: number\n  face: string\n  rear: string\n  wall: string\n  rims: string\n  rail: string\n  grooves: string[]\n  bolts: Vec2[]\n}\n\n/** One armour segment: the rail it rides, then the plate itself. */\nfunction Plate({\n  plate,\n  shell,\n  cast,\n  machined,\n  palette,\n  variant,\n}: {\n  plate: PlateArt\n  shell: React.SVGProps<SVGPathElement>\n  cast: React.SVGProps<SVGPathElement>\n  machined: React.SVGProps<SVGPathElement>\n  palette: { dark: string }\n  variant: RobotVariant\n}) {\n  return (\n    <g key={plate.index}>\n      <path data-rail={plate.index} d={plate.rail} {...machined} />\n      <path d={plate.rear} {...cast} />\n      {/* The side wall is a strip of real faces; only its rims are edges, so\n          it is filled once, shaded once, and never stroked quad by quad. */}\n      <path d={plate.wall} fill={shell.fill} fillOpacity={shell.fillOpacity} stroke=\"none\" />\n      {variant === \"solid\" && <path d={plate.wall} fill={palette.dark} opacity={0.16} stroke=\"none\" />}\n      <path d={plate.rims} fill=\"none\" stroke={shell.stroke} strokeWidth={0.6} opacity={0.7} />\n      <path data-plate={plate.index} d={plate.face} {...shell} />\n      <g fill=\"none\" stroke={palette.dark} strokeWidth={1} opacity={variant === \"solid\" ? 0.55 : 0.3}>\n        {plate.grooves.map((groove, index) => (\n          <path key={index} d={groove} />\n        ))}\n      </g>\n      {plate.bolts.map((bolt, index) => (\n        <circle key={index} cx={px(bolt.x)} cy={px(bolt.y)} r={1.4} fill={palette.dark} opacity={0.55} />\n      ))}\n    </g>\n  )\n}\n\n/* ------------------------------------------------------------------ maths */\n\nconst finite = (value: number, fallback = 0) => (Number.isFinite(value) ? value : fallback)\nconst finiteClamp = (value: number, min: number, max: number, fallback: number) =>\n  Number.isFinite(value) ? clamp(value, min, max) : fallback\nconst round3 = (value: number) => Number(value.toFixed(3))\n\nconst polar = (degrees: number, radius: number): Vec2 => ({\n  x: Math.cos(toRadians(degrees)) * radius,\n  y: Math.sin(toRadians(degrees)) * radius,\n})\n\n/** A quad wound so it always fills, whichever way the camera folded it. */\nconst facePath = (points: readonly Vec2[]) => {\n  let area = 0\n  for (let index = 0; index < points.length; index += 1) {\n    const a = points[index]\n    const b = points[(index + 1) % points.length]\n    area += a.x * b.y - b.x * a.y\n  }\n  return polygonPath(area < 0 ? [...points].reverse() : points)\n}\n\nconst polygonPath = (points: readonly Vec2[]) =>\n  points.length < 3\n    ? \"\"\n    : `${points.map((p, i) => `${i ? \"L\" : \"M\"} ${px(p.x)} ${px(p.y)}`).join(\" \")} Z`\n\nconst polylinePath = (points: readonly Vec2[]) =>\n  points.length < 2\n    ? \"\"\n    : points.map((p, i) => `${i ? \"L\" : \"M\"} ${px(p.x)} ${px(p.y)}`).join(\" \")\n\n/**\n * How far the casing outline stands from the optical axis along one bearing.\n * The outline is a rounded rectangle — the rectangle of half extents\n * `(w - r, h - r)` grown by `r` — so the boundary is where the distance from\n * that inner rectangle is exactly `r`, which bisects cleanly and lands on the\n * same number in Node and in the browser.\n */\nfunction outlineRadius(\n  degrees: number,\n  halfWidth: number,\n  halfHeight: number,\n  radius: number,\n): number {\n  const direction = polar(degrees, 1)\n  const innerW = halfWidth - radius\n  const innerH = halfHeight - radius\n  const outside = (t: number) => {\n    const x = Math.max(Math.abs(direction.x * t) - innerW, 0)\n    const y = Math.max(Math.abs(direction.y * t) - innerH, 0)\n    return Math.hypot(x, y) - radius\n  }\n  let low = 0\n  let high = Math.hypot(halfWidth, halfHeight) + radius\n  for (let step = 0; step < 24; step += 1) {\n    const mid = (low + high) / 2\n    if (outside(mid) > 0) high = mid\n    else low = mid\n  }\n  return (low + high) / 2\n}\n\n/** The casing's own reach on one bearing. */\nconst casingRadius = (degrees: number) =>\n  outlineRadius(degrees, CASE_HALF_W, CASE_HALF_H, CASE_RADIUS)\n\n/**\n * One armour segment's cross-section, in the face plane: the casing outline\n * across its own bearing, closed back along the inner arc. The segments *are*\n * the casing, so the outer edge is sampled from the outline rather than drawn\n * as its own shape.\n */\nfunction wedgeOutline(centre: number, half: number, outerScale: number): Vec2[] {\n  const steps = Math.max(4, Math.round(half / 5) + 3)\n  const outer = Array.from({ length: steps }, (_, step) => {\n    const angle = centre - half + (step * half * 2) / (steps - 1)\n    return polar(angle, casingRadius(angle) * outerScale)\n  })\n  const inner = Array.from({ length: steps }, (_, step) => {\n    const angle = centre + half - (step * half * 2) / (steps - 1)\n    return polar(angle, R_INNER)\n  })\n  return [...outer, ...inner]\n}\n\n/** A rounded rectangle about the optical axis, sampled for projection. */\nfunction rectOutline(halfWidth: number, halfHeight: number, radius: number): Vec2[] {\n  const r = Math.max(0, Math.min(radius, halfWidth, halfHeight))\n  const corners: Vec2[] = [\n    { x: halfWidth - r, y: -(halfHeight - r) },\n    { x: halfWidth - r, y: halfHeight - r },\n    { x: -(halfWidth - r), y: halfHeight - r },\n    { x: -(halfWidth - r), y: -(halfHeight - r) },\n  ]\n  if (r === 0) return corners\n  return corners.flatMap((corner, index) =>\n    Array.from({ length: 4 }, (_, step) => {\n      const angle = toRadians(index * 90 - 90 + (step * 90) / 3)\n      return { x: corner.x + Math.cos(angle) * r, y: corner.y + Math.sin(angle) * r }\n    }),\n  )\n}\n\nexport { CustodianDroid }\n\n/**\n * What it does with nothing on it. `open` is the shell the loop eases toward,\n * `lift` the float off the deck, `roll` the drift the shell turns through under\n * its own stabilized optic, and `voice` the ring output. All illustrative:\n * there is no thrust, no mass and no stroke load here.\n */\nexport function custodianDroidPose(behavior: CustodianDroidBehavior, clock: number) {\n  const time = Number.isFinite(clock) ? clock : 0\n  switch (behavior) {\n    // Half open and breathing, turning through the room as it goes.\n    case \"survey\":\n      return {\n        open: 0.5 + 0.16 * Math.sin(time * Math.PI * 2),\n        lift: 0.6 + 0.16 * Math.sin(time * Math.PI * 1.4),\n        roll: 0.7 * Math.sin(time * Math.PI * 0.5),\n        voice: 0,\n      }\n    // Shell thrown wide, tight fast float, talking.\n    case \"alert\":\n      return {\n        open: 0.86 + 0.1 * Math.sin(time * Math.PI * 6),\n        lift: 0.82 + 0.06 * Math.sin(time * Math.PI * 5),\n        roll: 0.25 * Math.sin(time * Math.PI * 3),\n        voice: 0.5 + 0.45 * Math.sin(time * 11),\n      }\n    case \"static\":\n      return { open: 0.35, lift: 0.5, roll: 0, voice: 0 }\n    // Armour seated, station-keeping, drifting round a bearing it never holds.\n    default:\n      return {\n        open: 0.06 + 0.05 * Math.sin(time * Math.PI * 2),\n        lift: 0.5 + 0.18 * Math.sin(time * Math.PI * 1.1),\n        roll: 0.45 * Math.sin(time * Math.PI * 0.37),\n        voice: 0,\n      }\n  }\n}\n",
      "type": "registry:ui",
      "target": "@ui/custodian-droid.tsx"
    }
  ],
  "categories": [
    "robotics",
    "droids"
  ],
  "type": "registry:ui"
}