{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pumpjack",
  "title": "Pumpjack",
  "description": "A beam pump solved as the four-bar it is: crank, pitman and walking beam close a real loop, and the polished-rod stroke falls out of the link lengths rather than being tweened. Crank, beam or air balance, and four camera views.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/linkage-geometry.json",
    "https://robocn.dev/r/use-robot-motion.json",
    "https://robocn.dev/r/assembly-geometry.json"
  ],
  "files": [
    {
      "path": "src/components/ui/pumpjack.tsx",
      "content": "\"use client\"\n\n/**\n * pumpjack — the beam pump, solved as the four-bar it is.\n *\n * The crank does not choose where the beam goes. `solveFourBar` closes the\n * loop — crank, pitman, walking beam — so the beam's swing, and therefore the\n * polished-rod stroke, is a *consequence* of the four link lengths rather than\n * a number anybody typed.\n *\n * The horsehead is why the rod stays vertical: its face is an arc centred on\n * the saddle bearing, so the wireline leaves it at a point fixed in space and\n * the carrier bar moves by exactly radius × beam angle. That is the stroke,\n * and it is computed, not tweened.\n *\n * `explode` takes it apart in the reverse of the order it was built, through\n * the shared `assembly-geometry` schedule. It parks the linkage first — an\n * exploded view of a *moving* four-bar is nonsense, since every part would be\n * floating off a seat that is itself swinging — and the angle it parks at is\n * scanned out of the loop rather than typed, because \"beam level\" is an\n * inverse four-bar problem. The handed pairs come off sideways, which reads in\n * `iso` and `front` and is foreshortened to almost nothing in `profile`, the\n * way a real side elevation cannot show a lateral move either.\n *\n * Nothing here is dynamics: no fluid, no rod load, no torque, no counterbalance\n * calculation. `balance` changes where the mass is drawn, not what the linkage\n * does.\n *\n * Design note: docs/pumpjack-teardown.md.\n */\n\nimport * as React from \"react\"\n\nimport { arrowStep, useRobotClock, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport {\n  assemblyEnvelope,\n  explodeAssembly,\n  type AssemblyPart,\n} from \"@/lib/robocn/assembly\"\nimport { clamp, toRadians, type Vec2, type Vec3 } from \"@/lib/robocn/kinematics\"\nimport {\n  rigidPoint,\n  solveFourBar,\n  type FourBarGeometry,\n} from \"@/lib/robocn/linkage\"\nimport {\n  boxCorners,\n  elevationDraft,\n  fitFrame,\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 PumpjackBehavior = \"pump\" | \"slow\" | \"service\" | \"static\"\n/** Where the counterbalance mass sits — the three arrangements that exist. */\nexport type PumpjackBalance = \"crank\" | \"beam\" | \"air\"\n/** Which axis a person grabs when the machine is interactive. */\nexport type PumpjackControl = \"crank\" | \"explode\"\n\nconst VIEW_WIDTH = 270\nconst VIEW_HEIGHT = 220\nconst NATIVE_VIEW: RobotView = \"profile\"\n\n/** Saddle bearing: the walking beam's pivot, on top of the samson post. */\nconst SADDLE: Vec2 = { x: 106, y: 124 }\n/** Gearbox output, which the crank turns about. */\nconst CRANK_PIVOT: Vec2 = { x: 162, y: 48 }\n\nconst GEOMETRY: FourBarGeometry = {\n  ground: SADDLE.x - CRANK_PIVOT.x,\n  rise: SADDLE.y - CRANK_PIVOT.y,\n  crank: 14,\n  coupler: 78,\n  rocker: 56,\n}\n\n/** Radius of the horsehead's face, and therefore how far out the well stands. */\nconst HEAD_RADIUS = 82\n/** The beam proper: saddle bearing to the horsehead's mounting flange. */\nconst BEAM_ARM = 70\nconst WELL_X = SADDLE.x - HEAD_RADIUS\n/** Carrier-bar height with the beam level. */\nconst CARRIER_REST = 88\n/** How far off centre the crank, its pitmans and its weights run. */\nconst CRANK_LATERAL = 26\n\nconst SEATED = { min: { x: -30, y: 0, z: -236 }, max: { x: 30, y: 176, z: 4 } }\nconst ENVELOPE = boxCorners(SEATED.min, SEATED.max)\n\n/* -------------------------------------------------------------------------- */\n/* the teardown                                                                */\n/* -------------------------------------------------------------------------- */\n\n/**\n * World axes, so the fit axes read the way `assembly-geometry` means them: `x`\n * starboard, `y` up, `z` aft. The drawing runs along `-z`, so the horsehead —\n * fitted onto the nose of the beam — comes off along `+z`.\n */\nconst UP: Vec3 = { x: 0, y: 1, z: 0 }\nconst DOWN: Vec3 = { x: 0, y: -1, z: 0 }\nconst FORE: Vec3 = { x: 0, y: 0, z: 1 }\nconst AFT: Vec3 = { x: 0, y: 0, z: -1 }\nconst PORT: Vec3 = { x: -1, y: 0, z: 0 }\nconst STARBOARD: Vec3 = { x: 1, y: 0, z: 0 }\n\n/**\n * The machine as it was put together, so it can be taken apart backwards. Order\n * 0 is the bench and comes off last; the rod is the highest order and comes off\n * first, which is the order a crew actually works in.\n *\n * The wellhead is absent on purpose: it is the well, not the pump. Nobody takes\n * the well apart to service the machine standing over it, so it stays put and\n * gives the drawing a fixed reference.\n */\nexport function pumpjackParts(): AssemblyPart[] {\n  return [\n    { id: \"skid\", axis: UP, travel: 0, order: 0 },\n    // Up the stack, each stage clears the one under it, so the travels have to\n    // grow with height or the parts land on top of each other at full spread.\n    { id: \"post\", axis: UP, travel: 20, order: 1 },\n    { id: \"gearbox\", axis: UP, travel: 30, order: 1 },\n    { id: \"mover\", axis: UP, travel: 52, order: 1 },\n    { id: \"saddle\", axis: UP, travel: 44, order: 2 },\n    { id: \"beam\", axis: UP, travel: 72, order: 3 },\n    { id: \"horsehead\", axis: FORE, travel: 56, order: 4 },\n    { id: \"equalizer\", axis: AFT, travel: 42, order: 4 },\n    // Outward in the order they come off, so the pitman clears the crank pin it\n    // was on and the weight clears the arm it was bolted to.\n    { id: \"crank-port\", axis: PORT, travel: 44, order: 5 },\n    { id: \"crank-starboard\", axis: STARBOARD, travel: 44, order: 5 },\n    { id: \"weight-port\", axis: PORT, travel: 62, order: 6 },\n    { id: \"weight-starboard\", axis: STARBOARD, travel: 62, order: 6 },\n    { id: \"pitman-port\", axis: PORT, travel: 80, order: 7 },\n    { id: \"pitman-starboard\", axis: STARBOARD, travel: 80, order: 7 },\n    { id: \"rod\", axis: DOWN, travel: 34, order: 8 },\n  ]\n}\n\n/**\n * The crank angle that stands the walking beam level, and the beam angle it\n * actually achieves. Scanned rather than typed: the loop is easy to solve\n * forwards and awkward backwards, and a quarter-degree sweep costs nothing once\n * at module scope. Exported so a test can hold the claim.\n */\nexport function pumpjackService(): { crankAngle: number; beamAngle: number } {\n  let crankAngle = 0\n  let beamAngle = Infinity\n  for (let angle = 0; angle < 360; angle += 0.25) {\n    const rocker = solveFourBar(angle, GEOMETRY, { branch: \"down\" }).rockerAngle\n    const beam = rocker > 180 ? rocker - 360 : rocker\n    if (Math.abs(beam) < Math.abs(beamAngle)) {\n      beamAngle = beam\n      crankAngle = angle\n    }\n  }\n  return { crankAngle, beamAngle }\n}\n\n/** Where the machine parks to be worked on. */\nexport const PUMPJACK_SERVICE_ANGLE = pumpjackService().crankAngle\n\nconst midpoint = (a: Vec2, b: Vec2): Vec2 => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 })\n\n/** Shortest way round from one angle to another, in degrees. */\nconst shortestArc = (from: number, to: number) => {\n  const delta = ((to - from) % 360 + 540) % 360 - 180\n  return delta\n}\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 PumpjackProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /** Controlled crank angle in degrees. Supplying it stops the loop. */\n  crankAngle?: number\n  onCrankAngleChange?: (angle: number) => void\n  behavior?: PumpjackBehavior\n  /**\n   * How far apart the machine is, 0 assembled to 1 fully exploded. Anything\n   * above 0 parks the linkage at the service angle and stops the loop.\n   */\n  explode?: number\n  onExplodeChange?: (explode: number) => void\n  /**\n   * How much the parts' travel windows overlap, 0 strictly one stage at a time\n   * to 1 everything at once.\n   */\n  explodeOverlap?: number\n  /** Dashed leaders from each displaced part back to its seat. */\n  showLeaders?: boolean\n  /** Which axis dragging and the arrow keys drive. */\n  control?: PumpjackControl\n  /** Where the counterbalance mass is carried. */\n  balance?: PumpjackBalance\n  /** Draw the wellhead, stuffing box and flow line under the polished rod. */\n  showWell?: boolean\n  showGround?: boolean\n  view?: RobotView\n  speed?: number\n  phase?: number\n  paused?: boolean\n  animate?: boolean\n  interactive?: boolean\n  label?: string\n  size?: RobotSize | number\n  variant?: RobotVariant\n}\n\nconst wrap360 = (value: number) =>\n  Number.isFinite(value) ? ((value % 360) + 360) % 360 : 0\n\nconst signed = (degrees: number) => (degrees > 180 ? degrees - 360 : degrees)\n\n/** The crank centre in viewBox units, for one camera. */\nfunction crankHub(view: RobotView): Vec2 {\n  const camera = robotCamera(view)\n  const frame = fitFrame(ENVELOPE, camera, VIEW_WIDTH, VIEW_HEIGHT)\n  return frame.toViewBox(camera.project(0, CRANK_PIVOT.y, -CRANK_PIVOT.x))\n}\n\nfunction Pumpjack({\n  crankAngle,\n  onCrankAngleChange,\n  behavior = \"pump\",\n  explode,\n  onExplodeChange,\n  explodeOverlap = 0.45,\n  showLeaders = true,\n  control = \"crank\",\n  balance = \"crank\",\n  showWell = true,\n  showGround = true,\n  view = NATIVE_VIEW,\n  speed = 0.35,\n  phase = 0,\n  paused = false,\n  animate = true,\n  interactive = false,\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  \"aria-label\": ariaLabel,\n  ...props\n}: PumpjackProps) {\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 [held, setHeld] = React.useState<number | null>(null)\n  const [tornDown, setTornDown] = React.useState<number | null>(null)\n  const controlled = crankAngle !== undefined\n  const grabsExplode = control === \"explode\"\n\n  const controlledExplode = explode !== undefined\n\n  const camera = robotCamera(view)\n\n  // The teardown runs on its own clock: `service` parks the crank, so there is\n  // no revolution for it to ride on.\n  const teardownClock = useRobotClock({\n    speed,\n    phase,\n    paused,\n    animate: animate && !controlledExplode && behavior === \"service\",\n  })\n  const apart = clamp(\n    controlledExplode\n      ? Number.isFinite(explode) ? (explode as number) : 0\n      : (tornDown ?? pumpjackTeardown(behavior, teardownClock)),\n    0,\n    1,\n  )\n\n  const parts = pumpjackParts()\n  // The frame grows with the teardown and with nothing else: it zooms out when\n  // the machine comes apart, never as the machine works.\n  const room = assemblyEnvelope(parts, apart, SEATED)\n  const frame = fitFrame(\n    apart > 0 ? boxCorners(room.min, room.max) : ENVELOPE,\n    camera,\n    VIEW_WIDTH,\n    VIEW_HEIGHT,\n  )\n  const { point: to, path: line, solid, box, bar, disc } = elevationDraft(camera, \"profile\")\n\n  const hold = controlled ? (Number.isFinite(crankAngle) ? (crankAngle as number) : 0) : held\n  const goal = React.useCallback(\n    (clock: number) => pumpjackCrank(behavior, clock),\n    [behavior],\n  )\n  const motion = useRobotScalar(goal, {\n    // The crank has to keep up with its own goal, or it lags a revolution\n    // behind and the beam stops matching the pitman.\n    rate: Math.max(240, Math.abs(speed) * 900),\n    hold,\n    speed,\n    paused,\n    phase,\n    animate: animate && !controlled && behavior !== \"static\" && apart === 0,\n  })\n  // An exploded view of a *moving* four-bar is nonsense — every part would be\n  // floating off a seat that is itself swinging — so the crank eases onto the\n  // service angle over the first sixth of the teardown, on the shortest arc, and\n  // is fully parked before anything has travelled far.\n  const running = wrap360(motion.value)\n  const parked = clamp(apart * 6, 0, 1)\n  const turn = parked === 0\n    ? running\n    : wrap360(running + shortestArc(running, PUMPJACK_SERVICE_ANGLE) * parked)\n\n  const apply = React.useCallback(\n    (next: number) => {\n      setHeld(next)\n      onCrankAngleChange?.(wrap360(next))\n    },\n    [onCrankAngleChange],\n  )\n  const applyExplode = React.useCallback(\n    (next: number) => {\n      setTornDown(clamp(next, 0, 1))\n      onExplodeChange?.(clamp(next, 0, 1))\n    },\n    [onExplodeChange],\n  )\n\n  // The pointer's bearing about the crank centre is the crank angle: grabbing\n  // it turns the gearbox by hand. The hub is recomputed from the camera inside\n  // the handler, so the only thing it closes over is the view.\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    onDrag: React.useCallback(\n      (unit: Vec2) => {\n        // Taking it apart is a straight pull across the frame; turning the\n        // gearbox is the pointer's bearing about the crank centre.\n        if (grabsExplode) {\n          applyExplode(unit.x)\n          return\n        }\n        const hub = crankHub(view)\n        const dx = unit.x * VIEW_WIDTH - hub.x\n        const dy = unit.y * VIEW_HEIGHT - hub.y\n        if (Math.hypot(dx, dy) < 4) return\n        apply((Math.atan2(-dy, dx) * 180) / Math.PI)\n      },\n      [apply, applyExplode, grabsExplode, view],\n    ),\n    onDragEnd: React.useCallback(() => setHeld(null), []),\n  })\n\n  const pose = solveFourBar(turn, GEOMETRY, { branch: \"down\" })\n  const local = (point: Vec2): Vec2 => ({\n    x: CRANK_PIVOT.x + point.x,\n    y: CRANK_PIVOT.y + point.y,\n  })\n  const crankPin = local(pose.crankPin)\n  const tailBearing = local(pose.couplerPin)\n  const beamAngle = signed(pose.rockerAngle)\n  const headAngle = beamAngle + 180\n  const beamHead = rigidPoint(SADDLE, headAngle, BEAM_ARM)\n  // The wireline leaves the face at a point fixed in space, so the rod moves by\n  // exactly the arc the face has rolled through: radius × beam angle.\n  const carrierY = CARRIER_REST - HEAD_RADIUS * toRadians(beamAngle)\n\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n\n  /** The horsehead face: an arc of the head radius, about the saddle bearing. */\n  const headFace = Array.from({ length: 9 }, (_, index) =>\n    rigidPoint(SADDLE, headAngle - 18 + (index / 8) * 36, HEAD_RADIUS),\n  )\n  const headBack = [\n    rigidPoint(SADDLE, headAngle + 17, BEAM_ARM - 8),\n    rigidPoint(SADDLE, headAngle - 17, BEAM_ARM - 8),\n  ]\n  const annotation = to(rigidPoint(CRANK_PIVOT, turn + 180, 48))\n  const readout = px(turn)\n\n  /* ---- the teardown ------------------------------------------------------ */\n\n  // Projection is linear, so a world offset is a pure screen offset — and a\n  // seated part emits no transform at all rather than an identity one.\n  const displaced = new Map<string, { transform?: string; rank: number; screen: Vec2 }>()\n  for (const part of explodeAssembly(parts, apart, { overlap: explodeOverlap })) {\n    const screen = camera.project(part.offset.x, part.offset.y, part.offset.z)\n    const still = px(screen.x) === 0 && px(screen.y) === 0\n    displaced.set(part.id, {\n      transform: still ? undefined : `translate(${px(screen.x)} ${px(screen.y)})`,\n      rank: part.rank,\n      screen,\n    })\n  }\n  /** Props for a part group: its displacement, its rank, and a test hook. */\n  const moved = (id: string) => {\n    const state = displaced.get(id)\n    return {\n      \"data-part\": id,\n      \"data-rank\": state?.rank ?? 0,\n      transform: state?.transform,\n    }\n  }\n  /**\n   * The leader back to the seat: the displacement negated. Drawn from where the\n   * part sits now, which is why it is inside the part's own transform.\n   */\n  const leader = (id: string, anchor: Vec2, depth = 0) => {\n    const state = displaced.get(id)\n    if (!showLeaders || !state || !state.transform) return null\n    const seat = to(anchor, depth)\n    return (\n      <path\n        data-leader={id}\n        d={`M ${px(seat.x)} ${px(seat.y)} L ${px(seat.x - state.screen.x)} ${px(seat.y - state.screen.y)}`}\n        fill=\"none\"\n        stroke={palette.grid}\n        strokeWidth={0.6}\n        strokeDasharray=\"3 3\"\n        opacity={0.75}\n      />\n    )\n  }\n\n  return (\n    <svg\n      ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={\n        ariaLabel ??\n        (apart > 0\n          ? `Beam pump, ${Math.round(apart * 100)} percent apart, parked at ${readout} degrees, ${viewNames[view] ?? viewNames.profile}`\n          : `Beam pump, crank at ${readout} degrees, carrier bar at ${px(carrierY)} units, ${viewNames[view] ?? viewNames.profile}`)\n      }\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? (grabsExplode ? 100 : 360) : undefined}\n      aria-valuenow={interactive ? (grabsExplode ? Math.round(apart * 100) : readout) : undefined}\n      aria-valuetext={\n        interactive\n          ? grabsExplode\n            ? `${Math.round(apart * 100)} percent apart`\n            : `crank at ${readout} degrees`\n          : undefined\n      }\n      tabIndex={tabIndex ?? (interactive ? 0 : undefined)}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (!interactive || event.defaultPrevented) return\n        if (grabsExplode) {\n          const step = arrowStep(event.key, event.shiftKey ? 0.1 : 0.04, 0.25)\n          if (step !== 0) applyExplode(apart + step)\n          else if (event.key === \"Home\") applyExplode(0)\n          else if (event.key === \"End\") applyExplode(1)\n          else return\n          event.preventDefault()\n          return\n        }\n        const delta = arrowStep(event.key, event.shiftKey ? 15 : 5, 45)\n        if (delta !== 0) apply(turn + delta)\n        else if (event.key === \"Home\") apply(0)\n        else if (event.key === \"End\") apply(180)\n        else return\n        event.preventDefault()\n      }}\n      onBlur={(event) => {\n        onBlur?.(event)\n        if (!dragging) setHeld(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      {...props}\n    >\n      {variant === \"blueprint\" && (\n        <path\n          d={`M 10 ${VIEW_HEIGHT - 22} H ${VIEW_WIDTH - 10}`}\n          fill=\"none\"\n          stroke={palette.grid}\n          strokeWidth={0.5}\n          strokeDasharray=\"3 4\"\n          opacity={0.4}\n        />\n      )}\n\n      <g data-view={view} data-exploded={px(apart)} transform={frame.transform || undefined}>\n        {showGround && (\n          <>\n            <path data-ground d={solid([{ x: -4, y: 0 }, { x: 236, y: 0 }], 30)} fill={palette.dark} opacity={0.12} />\n            <path d={line([{ x: -4, y: 0 }, { x: 236, y: 0 }])} fill=\"none\" stroke={palette.dark} strokeWidth={1} opacity={0.5} />\n          </>\n        )}\n\n        {/* Skid: everything above the grade bolts to it. */}\n        <g {...moved(\"skid\")}>\n          <path data-skid d={box(76, 0, 232, 11, 22)} {...cast} />\n        </g>\n\n        {/* Prime mover and belt guard, outboard of the gearbox. */}\n        {leader(\"mover\", { x: 213, y: 22 })}\n        <g {...moved(\"mover\")}>\n          <path d={box(198, 11, 228, 34, 14)} {...machined} />\n          <path d={disc({ x: 210, y: 23 }, 7, 3, 16)} {...cast} />\n          <path d={box(176, 15, 200, 32, 4, 12)} {...cast} fillOpacity={0.4} />\n        </g>\n\n        {/* Gearbox. The crank runs on its output shaft, outboard again. */}\n        {leader(\"gearbox\", { x: 164, y: 30 })}\n        <g {...moved(\"gearbox\")}>\n          <path data-gearbox d={box(140, 8, 188, 52, 18)} {...shell} />\n          <path d={box(146, 52, 182, 58, 16)} {...machined} />\n          <path d={line([{ x: 148, y: 18 }, { x: 180, y: 18 }])} fill=\"none\" stroke={palette.dark} strokeWidth={1.4} />\n          <path d={line([{ x: 148, y: 40 }, { x: 180, y: 40 }])} fill=\"none\" stroke={palette.dark} strokeWidth={1.4} />\n        </g>\n\n        {/* Samson post: an A-frame with two braces, carrying the saddle bearing. */}\n        {leader(\"post\", { x: 106, y: 68 })}\n        <g data-post {...moved(\"post\")}>\n          <path d={bar({ x: 86, y: 11 }, { x: SADDLE.x - 3, y: SADDLE.y - 4 }, 4, 16)} {...shell} />\n          <path d={bar({ x: 126, y: 11 }, { x: SADDLE.x + 3, y: SADDLE.y - 4 }, 4, 16)} {...shell} />\n          <path d={bar({ x: 92, y: 52 }, { x: 120, y: 52 }, 2.4, 14)} {...machined} />\n          <path d={bar({ x: 96, y: 86 }, { x: 116, y: 86 }, 2.2, 14)} {...machined} />\n        </g>\n\n        {/* Walking beam, on the saddle bearing; the horsehead slides off its nose. */}\n        {leader(\"beam\", rigidPoint(SADDLE, beamAngle, 20))}\n        <g data-beam data-angle={px(beamAngle)} {...moved(\"beam\")}>\n          <path d={bar(beamHead, tailBearing, 7, 9)} {...shell} />\n          <path d={bar(rigidPoint(SADDLE, beamAngle, -24), rigidPoint(SADDLE, beamAngle, 36), 3, 10)} {...machined} />\n          {balance === \"beam\" && (\n            <path\n              data-counterweight\n              d={bar(rigidPoint(SADDLE, beamAngle, BEAM_ARM - 6), rigidPoint(SADDLE, beamAngle, BEAM_ARM + 14), 12, 11)}\n              {...shell}\n            />\n          )}\n        </g>\n        {leader(\"horsehead\", beamHead)}\n        <g {...moved(\"horsehead\")}>\n          <path data-horsehead d={solid([...headFace, ...headBack], 9)} {...shell} />\n          <path d={line(headFace, 9)} fill=\"none\" stroke={palette.dark} strokeWidth={1.2} opacity={0.8} />\n        </g>\n        {leader(\"equalizer\", tailBearing)}\n        <g {...moved(\"equalizer\")}>\n          <path data-equalizer d={disc(tailBearing, 5, CRANK_LATERAL + 2)} {...cast} />\n        </g>\n        {leader(\"saddle\", SADDLE)}\n        <g {...moved(\"saddle\")}>\n          <path d={disc(SADDLE, 9, 12)} {...cast} />\n          <path d={disc(SADDLE, 3.4, 13)} {...machined} />\n        </g>\n\n        {/* Bridle, carrier bar, polished rod. */}\n        {leader(\"rod\", { x: WELL_X, y: carrierY })}\n        <g data-rod data-position={px(carrierY)} {...moved(\"rod\")}>\n          {[-5, 5].map((offset) => (\n            <path\n              key={offset}\n              d={line([{ x: WELL_X, y: SADDLE.y }, { x: WELL_X, y: carrierY }], offset)}\n              fill=\"none\"\n              stroke={palette.metal}\n              strokeWidth={1.2}\n            />\n          ))}\n          <path d={box(WELL_X - 9, carrierY - 3, WELL_X + 9, carrierY + 3, 7)} {...machined} />\n          <path d={bar({ x: WELL_X, y: carrierY }, { x: WELL_X, y: 34 }, 2, 2)} {...machined} />\n        </g>\n\n        {showWell && (\n          <g data-wellhead>\n            <path d={box(WELL_X - 6, 22, WELL_X + 6, 34, 6)} {...machined} />\n            <path d={box(WELL_X - 9, 8, WELL_X + 9, 22, 9)} {...shell} />\n            <path d={box(WELL_X - 13, 0, WELL_X + 13, 8, 13)} {...cast} />\n            <path\n              data-flow\n              d={line([{ x: WELL_X - 9, y: 15 }, { x: -2, y: 15 }])}\n              fill=\"none\"\n              stroke={palette.accent}\n              strokeWidth={3}\n              strokeLinecap=\"round\"\n            />\n            <path d={disc({ x: WELL_X - 22, y: 15 }, 4.5, 5)} {...cast} />\n          </g>\n        )}\n\n        {/* Crank, pitman and counterbalance: a handed pair either side of the\n            gearbox, and the one place in the set where parts come off sideways.\n            Port draws before starboard, which is the right order at every one of\n            the four cameras — none of them has +x further away than -x. */}\n        {([-CRANK_LATERAL, CRANK_LATERAL] as const).map((offset) => {\n          const side = offset < 0 ? \"port\" : \"starboard\"\n          return (\n            <g key={offset} data-crank data-side={offset < 0 ? \"left\" : \"right\"}>\n              {balance === \"crank\" && (\n                <>\n                  {leader(`weight-${side}`, rigidPoint(CRANK_PIVOT, turn + 180, 21), offset)}\n                  <g {...moved(`weight-${side}`)}>\n                    <path\n                      data-counterweight\n                      d={bar(rigidPoint(CRANK_PIVOT, turn + 180, 14), rigidPoint(CRANK_PIVOT, turn + 180, 28), 10, 6, offset)}\n                      {...shell}\n                    />\n                  </g>\n                </>\n              )}\n              {leader(`crank-${side}`, rigidPoint(CRANK_PIVOT, turn + 180, 30), offset)}\n              <g {...moved(`crank-${side}`)}>\n                <path d={bar(rigidPoint(CRANK_PIVOT, turn + 180, 30), crankPin, 5, 3, offset)} {...machined} />\n                <path d={disc(crankPin, 3.4, 4, offset)} {...cast} />\n              </g>\n              {leader(`pitman-${side}`, midpoint(crankPin, tailBearing), offset)}\n              <g {...moved(`pitman-${side}`)}>\n                <path data-pitman d={bar(crankPin, tailBearing, 3.4, 3, offset)} {...machined} />\n              </g>\n            </g>\n          )\n        })}\n        {/* The output shaft: the gearbox again, drawn after the cranks so it\n            caps them. It carries the same displacement without repeating the id. */}\n        <g transform={displaced.get(\"gearbox\")?.transform}>\n          <path d={disc(CRANK_PIVOT, 8, CRANK_LATERAL + 6)} {...cast} />\n        </g>\n\n        {balance === \"air\" && (\n          <g data-counterweight transform={displaced.get(\"mover\")?.transform}>\n            <path d={box(178, 11, 198, 32, 8)} {...cast} />\n            <path d={bar({ x: 188, y: 30 }, rigidPoint(SADDLE, beamAngle, 44), 4, 6)} {...machined} />\n          </g>\n        )}\n\n        {variant === \"blueprint\" && (\n          <>\n            <g fill=\"none\" stroke={palette.grid} strokeWidth={0.6} opacity={0.7}>\n              <path d={`${line([CRANK_PIVOT, crankPin, tailBearing, SADDLE])} Z`} strokeDasharray=\"4 3\" />\n              <path d={line([SADDLE, beamHead])} strokeDasharray=\"4 3\" />\n              <path d={line([{ x: WELL_X, y: 40 }, { x: WELL_X, y: 118 }])} strokeDasharray=\"2 3\" />\n            </g>\n            <text\n              x={px(annotation.x)}\n              y={px(annotation.y)}\n              textAnchor=\"middle\"\n              fontFamily=\"ui-monospace, monospace\"\n              fontSize={6}\n              fill={palette.foreground}\n            >\n              {px(pose.transmissionAngle)}°\n            </text>\n          </>\n        )}\n      </g>\n\n      {label && (\n        <text\n          x={VIEW_WIDTH / 2}\n          y={VIEW_HEIGHT - 7}\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/**\n * Crank angle at `clock`, unwrapped so the easing never has to cross a seam.\n *\n * `pump` is one revolution a cycle. `slow` is what a pump-off controller does:\n * two revolutions, then a rest — a real duty cycle rather than the same motion\n * at a different rate.\n */\nexport function pumpjackCrank(behavior: PumpjackBehavior, clock: number) {\n  // Parked for service: the crank holds while the machine comes apart.\n  if (behavior === \"service\") return PUMPJACK_SERVICE_ANGLE\n  if (behavior === \"static\" || !Number.isFinite(clock)) return 0\n  if (behavior === \"slow\") {\n    const whole = Math.floor(clock)\n    return whole * 720 + Math.min((clock - whole) / 0.6, 1) * 720\n  }\n  return clock * 360\n}\n\n/**\n * How far apart the machine is at `clock`. Only `service` takes it apart, and it\n * runs the teardown out and back over one cycle so the assembly is the resting\n * state at both ends rather than only at the start.\n */\nexport function pumpjackTeardown(behavior: PumpjackBehavior, clock: number) {\n  if (behavior !== \"service\" || !Number.isFinite(clock)) return 0\n  const cycle = ((clock % 1) + 1) % 1\n  return 1 - Math.abs(2 * cycle - 1)\n}\n\n/** Carrier-bar height for a crank angle: the stroke, solved through the loop. */\nexport function pumpjackCarrier(crankAngle: number) {\n  const pose = solveFourBar(crankAngle, GEOMETRY, { branch: \"down\" })\n  return CARRIER_REST - HEAD_RADIUS * toRadians(signed(pose.rockerAngle))\n}\n\nexport { Pumpjack }\n",
      "type": "registry:ui",
      "target": "@ui/pumpjack.tsx"
    }
  ],
  "categories": [
    "robotics",
    "energy"
  ],
  "type": "registry:ui"
}