{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "robot-torso",
  "title": "Torso",
  "description": "A pelvis, a column of equal vertebrae, and a cage of rib hoops that opens along the machine's depth as it breathes. Leaning and twisting move the shoulders without stretching the back. Drawn from any of four camera angles.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/skeleton-kinematics.json",
    "https://robocn.dev/r/use-robot-motion.json",
    "https://robocn.dev/r/robot-kinematics.json"
  ],
  "files": [
    {
      "path": "src/components/ui/robot-torso.tsx",
      "content": "\"use client\"\n\n/**\n * robot-torso — a pelvis, a column of vertebrae, and the cage they carry.\n *\n * The spine is `spineCurve` from `skeleton-kinematics`: equal segments from the\n * sacrum to the shoulder line, each carrying its share of the lean plus a fixed\n * lordotic shape, and each carrying its share of the twist. Leaning and\n * twisting therefore move the shoulders without stretching the back — every\n * vertebra is the same distance from the one below it in every pose.\n *\n * The ribs are hoops hung off the thoracic vertebrae and drawn in the\n * transverse plane, so they foreshorten into the ellipses a cage actually makes\n * from a raised camera instead of staying drawn-on arcs. `breath` opens them\n * along the machine's depth more than across its width, which is the direction\n * a pump moves a cage.\n *\n * There is no balance and no mass here. `lean` is a number someone typed.\n */\n\nimport * as React from \"react\"\n\nimport { arrowStep, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport { clamp, lerp, toRadians, type Vec2, type Vec3 } from \"@/lib/robocn/kinematics\"\nimport { defaultProportions, spineCurve } from \"@/lib/robocn/skeleton\"\nimport {\n  capsulePath,\n  px,\n  resolveRobotPalette,\n  resolveRobotSize,\n  robotCamera,\n  robotSurface,\n  slabPath,\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 TorsoBehavior = \"breathe\" | \"twist\" | \"static\"\n\nconst VIEW_WIDTH = 190\nconst VIEW_HEIGHT = 220\n/** World origin on screen: the sacrum, where the column starts. */\nconst CENTRE = { x: 95, y: 166 }\nconst SCALE = 1.75\nconst SLEW_RATE = 1.2\nconst NATIVE_VIEW: RobotView = \"front\"\n\nconst P = defaultProportions\n/** Half-width and half-depth of the widest rib hoop. */\nconst RIB_WIDTH = 22\nconst RIB_DEPTH = 15\n/** Where the cage sits on the column, as spine fractions. */\nconst RIB_FROM = 0.4\nconst RIB_TO = 0.94\n\nconst fits: Record<RobotView, number> = { plan: 1, front: 1, profile: 1, iso: 0.88 }\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 RobotTorsoProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /** Controlled pitch in degrees, positive leaning toward the nose. */\n  lean?: number\n  /** Controlled shoulder counter-rotation against the pelvis, in degrees. */\n  twist?: number\n  /** Controlled lateral roll in degrees, positive toward the machine's right. */\n  sway?: number\n  /** Controlled cage expansion, 0 empty to 1 full. */\n  breath?: number\n  /** What the torso does when none of the pose props are supplied. */\n  behavior?: TorsoBehavior\n  /** How many rib hoops the cage is built from. */\n  ribs?: number\n  /** Where the camera stands. One torso, four projections. */\n  view?: RobotView\n  /** Breaths, or twists, per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  phase?: number\n  /** Drag across to twist and up and down to lean. */\n  interactive?: boolean\n  onLeanChange?: (lean: number) => void\n  onTwistChange?: (twist: number) => void\n  /** Draw the pelvis casting and its hip sockets. */\n  showHips?: boolean\n  label?: string\n  size?: RobotSize | number\n  variant?: RobotVariant\n}\n\nfunction RobotTorso({\n  lean,\n  twist,\n  sway,\n  breath,\n  behavior = \"breathe\",\n  ribs = 7,\n  view = NATIVE_VIEW,\n  speed = 0.35,\n  animate = true,\n  paused = false,\n  phase = 0,\n  interactive = false,\n  onLeanChange,\n  onTwistChange,\n  showHips = true,\n  label,\n  size = \"md\",\n  variant = \"solid\",\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}: RobotTorsoProps) {\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 [grabbed, setGrabbed] = React.useState<{ lean: number; twist: number } | null>(null)\n\n  const controlled = lean !== undefined || twist !== undefined || breath !== undefined\n  const hold = controlled ? finite(breath, 0.3) : grabbed ? 0.3 : null\n  const goal = React.useCallback((clock: number) => torsoBreath(behavior, clock), [behavior])\n  const motion = useRobotScalar(goal, {\n    rate: Math.max(SLEW_RATE, Math.abs(speed) * 4),\n    hold: hold === null ? null : clamp(hold, 0, 1),\n    speed,\n    animate: animate && !controlled && behavior !== \"static\",\n    paused,\n    phase,\n  })\n  const open = clamp(Number.isFinite(motion.value) ? motion.value : 0, 0, 1)\n\n  const pitch = clamp(\n    lean !== undefined ? finite(lean, 0) : (grabbed?.lean ?? torsoLean(behavior, motion.clock)),\n    -35,\n    45,\n  )\n  const turn = clamp(\n    twist !== undefined ? finite(twist, 0) : (grabbed?.twist ?? torsoTwist(behavior, motion.clock)),\n    -40,\n    40,\n  )\n  const roll = clamp(finite(sway, 0), -25, 25)\n\n  const setPose = React.useCallback(\n    (next: { lean: number; twist: number }) => {\n      const bounded = {\n        lean: clamp(next.lean, -35, 45),\n        twist: clamp(next.twist, -40, 40),\n      }\n      setGrabbed(bounded)\n      onLeanChange?.(bounded.lean)\n      onTwistChange?.(bounded.twist)\n    },\n    [onLeanChange, onTwistChange, setGrabbed],\n  )\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    onDrag: React.useCallback(\n      (unit: Vec2) => setPose({ lean: (unit.y - 0.5) * 70, twist: (unit.x - 0.5) * 80 }),\n      [setPose],\n    ),\n    onDragEnd: React.useCallback(() => {}, []),\n  })\n\n  const sacrum: Vec3 = { x: 0, y: 0, z: 0 }\n  const spine = spineCurve({\n    base: sacrum,\n    length: P.spine,\n    segments: Math.max(3, Math.min(12, Math.round(finite(ribs, 7) + 2))),\n    lean: pitch,\n    twist: turn,\n  })\n  const shoulders = spine[spine.length - 1]\n\n  const camera = robotCamera(view)\n  const fit = fits[view] ?? 1\n  /** Lateral sway is a roll of the whole column about the sacrum. */\n  const lean2 = (point: Vec3): Vec3 => {\n    if (!roll) return point\n    const a = toRadians(roll)\n    const c = Math.cos(a)\n    const s = Math.sin(a)\n    return { x: point.x * c - point.y * s, y: point.x * s + point.y * c, z: point.z }\n  }\n  const to = (point: Vec3): Vec2 => {\n    const p = lean2(point)\n    return camera.project(p.x, p.y, p.z)\n  }\n  const box = (centre: Vec3, hx: number, hy: number, hz: number) =>\n    slabPath(\n      [-1, 1].flatMap((sx) =>\n        [-1, 1].flatMap((sy) =>\n          [-1, 1].map((sz) =>\n            lean2({\n              x: centre.x + sx * hx,\n              y: centre.y + sy * hy,\n              z: centre.z + sz * hz,\n            }),\n          ),\n        ),\n      ),\n      camera,\n    )\n  const link = (a: Vec3, b: Vec3, radius: number) => capsulePath(to(a), to(b), radius)\n\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n  /** A hoop is an outline, not a solid: paint its stroke, never its inside. */\n  const hoop = (weight: number) => {\n    const surface = robotSurface(\"shell\", variant, palette)\n    return {\n      fill: \"none\",\n      stroke: variant === \"wire\" ? palette.grid : palette.shell,\n      strokeWidth: variant === \"solid\" ? weight : surface.strokeWidth,\n      strokeDasharray: surface.strokeDasharray,\n      strokeLinejoin: \"round\" as const,\n    }\n  }\n\n  const count = Math.max(3, Math.min(10, Math.round(finite(ribs, 7))))\n  const spineAt = (fraction: number): Vec3 => {\n    const t = clamp(fraction, 0, 1) * (spine.length - 1)\n    const i = Math.min(spine.length - 2, Math.floor(t))\n    const f = t - i\n    return {\n      x: lerp(spine[i].x, spine[i + 1].x, f),\n      y: lerp(spine[i].y, spine[i + 1].y, f),\n      z: lerp(spine[i].z, spine[i + 1].z, f),\n    }\n  }\n  const yawAt = (fraction: number) => turn * clamp(fraction, 0, 1)\n\n  // Each rib is a pair of open arcs leaving its own vertebra, sweeping out and\n  // round, and descending as they come forward — which is what makes a front\n  // elevation of a cage read as ribs rather than as a stack of rings. The\n  // lowest hoops float: they fall away further and stop short of the midline.\n  const cage = Array.from({ length: count }, (_, index) => {\n    const k = count === 1 ? 0 : index / (count - 1)\n    const fraction = lerp(RIB_FROM, RIB_TO, k)\n    const centre = spineAt(fraction)\n    const profile = 0.42 + 0.58 * Math.sin(Math.PI * (0.16 + 0.78 * (1 - k)))\n    const half = RIB_WIDTH * profile * (1 + 0.09 * open)\n    const deep = RIB_DEPTH * profile * (1 + 0.24 * open)\n    const yaw = toRadians(yawAt(fraction))\n    const floating = clamp((0.24 - k) / 0.24, 0, 1)\n    /** How far short of the front midline this hoop stops, in radians. */\n    const gap = lerp(0.2, 0.85, floating)\n    const drop = lerp(5, 8.5, floating) * (1 + 0.12 * open)\n    const place = (a: number): Vec3 => {\n      const x = Math.sin(a) * half\n      // `a` runs from the vertebra at the back round to the front midline.\n      const z = -Math.cos(a) * deep\n      return {\n        x: centre.x + x * Math.cos(yaw) - z * Math.sin(yaw),\n        y: centre.y + 1.6 * open - drop * (1 - Math.abs(a) / Math.PI),\n        z: centre.z + x * Math.sin(yaw) + z * Math.cos(yaw),\n      }\n    }\n    // The arc starts just off the midline at the back, so a rib is a rib and\n    // not a ring seen through itself.\n    const arc = (sign: number) =>\n      Array.from({ length: 13 }, (_, step) => place(lerp(Math.PI * 0.93, gap, step / 12) * sign))\n    return {\n      index,\n      k,\n      floating,\n      centre,\n      left: arc(-1),\n      right: arc(1),\n      front: place(gap * 0.999),\n    }\n  })\n  const attached = cage.filter((rib) => rib.floating < 0.5)\n  const sternumTop = attached.length ? attached[attached.length - 1].front : shoulders\n  const sternumFoot = attached.length ? attached[0].front : spineAt(RIB_FROM)\n\n  const yoke = (sign: number): Vec3 => {\n    const yaw = toRadians(turn)\n    const span = sign * P.shoulderSpan\n    return {\n      x: shoulders.x + span * Math.cos(yaw),\n      y: shoulders.y,\n      z: shoulders.z + span * Math.sin(yaw),\n    }\n  }\n  const left = yoke(-1)\n  const right = yoke(1)\n  const neckTop: Vec3 = { x: shoulders.x, y: shoulders.y + P.neck, z: shoulders.z - P.neck * 0.14 }\n  const hip = (sign: number): Vec3 => ({ x: sign * P.hipSpan, y: -9, z: 1 })\n\n  return (\n    <svg\n      ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={`Robot torso, leaning ${Math.round(pitch)} degrees and twisted ${Math.round(turn)} degrees, ${count} rib hoops, ${viewNames[view] ?? viewNames.front}`}\n      aria-valuemin={interactive ? -40 : undefined}\n      aria-valuemax={interactive ? 40 : undefined}\n      aria-valuenow={interactive ? Math.round(turn) : undefined}\n      aria-valuetext={interactive ? `twisted ${Math.round(turn)} degrees, leaning ${Math.round(pitch)}` : undefined}\n      tabIndex={tabIndex ?? (interactive ? 0 : undefined)}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (!interactive || event.defaultPrevented) return\n        const delta = arrowStep(event.key, 4, 12)\n        const sideways = event.key === \"ArrowLeft\" || event.key === \"ArrowRight\"\n        if (delta !== 0) {\n          setPose({\n            lean: pitch + (sideways ? 0 : delta),\n            twist: turn + (sideways ? delta : 0),\n          })\n        } else if (event.key === \"Home\") setPose({ lean: 0, twist: 0 })\n        else return\n        event.preventDefault()\n      }}\n      onBlur={(event) => {\n        onBlur?.(event)\n        if (!dragging) setGrabbed(null)\n      }}\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      data-view={view}\n      {...props}\n    >\n      <g data-torso transform={`translate(${CENTRE.x} ${CENTRE.y}) scale(${px(SCALE * fit)})`}>\n        {showHips && (\n          <g data-pelvis>\n            <path d={box({ x: 0, y: -7, z: 0 }, 15, 9.5, 9)} {...shell} />\n            <path d={box({ x: 0, y: 2.5, z: 0 }, 10, 5, 7)} {...machined} />\n            {[-1, 1].map((sign) => (\n              <circle\n                key={sign}\n                data-socket={sign < 0 ? \"left\" : \"right\"}\n                cx={px(to(hip(sign)).x)}\n                cy={px(to(hip(sign)).y)}\n                r={3.8}\n                {...cast}\n              />\n            ))}\n          </g>\n        )}\n\n        <g data-spine>\n          {spine.map((vertebra, index) => (\n            <g key={index} data-vertebra={index}>\n              {index > 0 && (\n                <path d={link(spine[index - 1], vertebra, 2.2)} {...cast} />\n              )}\n              <path d={box(vertebra, 3.6, 1.9, 3)} {...machined} />\n            </g>\n          ))}\n        </g>\n\n        <g data-ribcage>\n          {cage.map((rib) => (\n            <g key={rib.index} data-rib={rib.index}>\n              {[rib.left, rib.right].map((arc, half) => (\n                <path\n                  key={half}\n                  d={arc\n                    .map((point, i) => {\n                      const at = to(point)\n                      return `${i ? \"L\" : \"M\"} ${px(at.x)} ${px(at.y)}`\n                    })\n                    .join(\" \")}\n                  {...hoop(3.1)}\n                />\n              ))}\n            </g>\n          ))}\n        </g>\n\n        <g data-sternum>\n          <path d={link(sternumFoot, sternumTop, 4.4)} {...machined} />\n          <path d={box(sternumTop, 5, 3.4, 2.6)} {...shell} />\n          <circle\n            cx={px(to(midpoint(sternumFoot, sternumTop)).x)}\n            cy={px(to(midpoint(sternumFoot, sternumTop)).y)}\n            r={3.6}\n            fill={palette.accent}\n            fillOpacity={px(0.35 + open * 0.65)}\n          />\n        </g>\n\n        <g data-shoulders>\n          <path d={link(left, right, 3.4)} {...machined} />\n          {[\n            [\"left\", left],\n            [\"right\", right],\n          ].map(([name, point]) => (\n            <g key={name as string} data-shoulder={name as string}>\n              <path d={box(point as Vec3, 6, 4.6, 6)} {...shell} />\n              <circle\n                cx={px(to(point as Vec3).x)}\n                cy={px(to(point as Vec3).y)}\n                r={3.6}\n                {...cast}\n              />\n            </g>\n          ))}\n        </g>\n\n        <g data-neck>\n          <path d={link(shoulders, neckTop, 4.2)} {...machined} />\n          <path d={box(neckTop, 5.5, 2.2, 4.5)} {...cast} />\n        </g>\n\n        {variant === \"blueprint\" && (\n          <g fill=\"none\" stroke={palette.grid} strokeWidth={0.4} opacity={0.7}>\n            <path\n              d={`M ${px(to({ x: 0, y: -18, z: 0 }).x)} ${px(to({ x: 0, y: -18, z: 0 }).y)} L ${px(to({ x: 0, y: P.spine + P.neck + 6, z: 0 }).x)} ${px(to({ x: 0, y: P.spine + P.neck + 6, z: 0 }).y)}`}\n              strokeDasharray=\"2 3\"\n            />\n          </g>\n        )}\n      </g>\n\n      <g fontFamily=\"ui-monospace, monospace\" textAnchor=\"middle\" fill={palette.foreground}>\n        <text x={VIEW_WIDTH / 2} y={205} fontSize={5}>\n          {`LEAN ${Math.round(pitch)}° / TWIST ${Math.round(turn)}° / BREATH ${Math.round(open * 100)}%`}\n        </text>\n        {label && (\n          <text x={VIEW_WIDTH / 2} y={214} fontSize={4.5}>\n            {label}\n          </text>\n        )}\n      </g>\n    </svg>\n  )\n}\n\nconst midpoint = (a: Vec3, b: Vec3): Vec3 => ({\n  x: (a.x + b.x) / 2,\n  y: (a.y + b.y) / 2,\n  z: (a.z + b.z) / 2,\n})\n\nconst finite = (value: number | undefined, fallback: number) =>\n  typeof value === \"number\" && Number.isFinite(value) ? value : fallback\n\n/** Cage expansion at `clock`: in for a third of the cycle, out for the rest. */\nexport function torsoBreath(behavior: TorsoBehavior, clock: number) {\n  if (behavior === \"static\" || !Number.isFinite(clock)) return 0.3\n  if (behavior === \"twist\") return 0.35\n  const t = ((clock % 1) + 1) % 1\n  return t < 0.38 ? t / 0.38 : 1 - (t - 0.38) / 0.62\n}\n\n/** Shoulder counter-rotation at `clock`, in degrees. */\nexport function torsoTwist(behavior: TorsoBehavior, clock: number) {\n  if (behavior !== \"twist\" || !Number.isFinite(clock)) return 0\n  return Math.sin(((clock % 1) + 1) % 1 * Math.PI * 2) * 32\n}\n\n/** Whole-column pitch at `clock`, in degrees. */\nexport function torsoLean(behavior: TorsoBehavior, clock: number) {\n  if (behavior === \"static\" || !Number.isFinite(clock)) return 0\n  if (behavior === \"twist\") return 6\n  return 3 + Math.sin(((clock % 1) + 1) % 1 * Math.PI * 2) * 2.5\n}\n\nexport { RobotTorso }\n",
      "type": "registry:ui",
      "target": "@ui/robot-torso.tsx"
    }
  ],
  "categories": [
    "robotics",
    "body"
  ],
  "type": "registry:ui"
}