{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "robot-horse",
  "title": "Horse",
  "description": "A quadruped whose gait is the mechanism: walk, trot, pace, canter and gallop are real footfall sequences whose beat is counted from them, and the load each grounded foot carries drives two joints nobody sets — a sprung fetlock that sinks under weight and recoils free, and a neck that nods because the forehand is loading and unloading. Drag across it to scrub the stride one footfall at a time.",
  "registryDependencies": [
    "https://robocn.dev/r/gait-kinematics.json",
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/spine-kinematics.json",
    "https://robocn.dev/r/use-pointer-target.json",
    "https://robocn.dev/r/use-robot-motion.json"
  ],
  "files": [
    {
      "path": "src/components/ui/robot-horse.tsx",
      "content": "\"use client\"\n\n/**\n * robot-horse — the gait is the mechanism, and the load is what it produces.\n *\n * Five machines in this set already have four legs on the floor, and all five\n * answer *what moves a leg root*. This one does not ask that. Its back is a\n * plain solved topline and its withers are the anchor. What is new is\n * underneath: a **named gait is a real footfall sequence** — `solveGait` gives\n * the touchdown instant of every limb, counts the beat from them, and works out\n * what share of the standing weight each grounded foot is carrying.\n *\n * That share then drives two things nobody sets. **The fetlock is a spring**:\n * the pastern's angle is `fetlockSink(load)`, so a loaded limb visibly sinks\n * and a swinging one recoils, and what you are watching is the support pattern\n * made visible. **The neck is a balance beam**: its carriage answers the\n * forehand's loading, so a walking horse nods once a stride and a trotting one\n * barely nods, out of the same arithmetic rather than two scripts. Drag across\n * it and you scrub the stride one footfall at a time.\n *\n * Design note: docs/equine-robots.md.\n */\n\nimport * as React from \"react\"\n\nimport { usePointerTarget } from \"@/hooks/use-pointer-target\"\nimport { useRobotClock, useRobotDrag } from \"@/hooks/use-robot-motion\"\nimport {\n  clamp,\n  lerp,\n  lerp2,\n  normalize2,\n  rotate2,\n  solveChain2,\n  toDegrees,\n  toRadians,\n  type Vec2,\n} from \"@/lib/robocn/kinematics\"\nimport {\n  fetlockSink,\n  solveGait,\n  type EquineGait,\n  type GaitLead,\n  type GaitLeg,\n  type GaitLegId,\n} from \"@/lib/robocn/gait\"\nimport { solveSpine, spineLimits, type SpinePose } from \"@/lib/robocn/spine\"\nimport {\n  aboutPoint,\n  capsulePath,\n  extrudedPath,\n  px,\n  resolveRobotPalette,\n  resolveRobotSize,\n  robotCamera,\n  robotSurface,\n  roundedFootprint,\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 HorseBehavior = \"walk\" | \"trot\" | \"canter\" | \"gallop\" | \"graze\" | \"static\"\n\n/** Drawn in side elevation; that is the camera it defaults to. */\nconst NATIVE_VIEW: RobotView = \"profile\"\n\n/** Where the body's centre stands in the frame, and the floor underneath it. */\nconst ORIGIN = 118\nconst GROUND = 150\nconst BOX = { width: 260, height: 180 } as const\n/** Half the track: the legs are either side of the trunk. */\nconst HALF_TRACK = 11\n\n/** Withers to croup along the back, and how high the withers stand. */\nconst TRUNK = 52\nconst WITHERS = 74\n/** Humerus then radius to the knee, and the rigid cannon below it. */\nconst FORE = [22, 24] as const\nconst FORE_CANNON = 17\n/** Femur then tibia to the hock, and the rigid metatarsus below it. */\nconst HIND = [23, 24] as const\nconst HIND_CANNON = 20\n/** The pastern: the one link in the limb whose angle is an output. */\nconst PASTERN = 10\n/** How far off the horizontal an unloaded pastern stands, in degrees. */\nconst PASTERN_REST = 58\n/** Cervical chain: withers to the poll, solved. */\nconst NECK = [24, 19] as const\n/** Where the poll is put, relative to the body axis: head to the floor, head up. */\nconst NECK_ANGLE = { down: -85, up: 68 } as const\n/** And how far out, kept close to the chain's own reach so the crest arches\n *  instead of folding. */\nconst NECK_SPAN = { down: 40, up: 38.5 } as const\n/** The dock and the skirt hung off the croup. */\nconst TAIL = 52\n/** Where the dock leaves the croup, in degrees off the body's own axis:\n *  hanging at rest, clamped under the quarters, or flagged out behind. */\nconst TAIL_SET = { rest: 78, clamped: 108, flagged: 2 } as const\n/** How far a full suspension lifts the whole machine, in world units. */\nconst RISE = 14\n/** How far the neck's carriage swings between a loaded and an unloaded forehand. */\nconst NOD = 1.8\n/** Half the span between the ear axes, and the ear itself. */\nconst EAR_ACROSS = 3.8\nconst EAR_HEIGHT = 8.5\nconst EAR_HALF = 2.9\n\n/** How far the camera pulls back so the machine still fits a frame drawn for one view. */\nconst fits: Record<RobotView, number> = { plan: 1.2, front: 1.15, profile: 1, iso: 0.95 }\n\nconst viewNames: Record<RobotView, string> = {\n  plan: \"plan view\",\n  front: \"front elevation\",\n  profile: \"side elevation\",\n  iso: \"isometric view\",\n}\n\n/** How the beat count reads out loud. */\nconst beatNames = [\"\", \"one-beat\", \"two-beat\", \"three-beat\", \"four-beat\"] as const\n\n/**\n * The side elevation's own camera, kept at module scope. The ears pan out of\n * the plane the machine is drawn in, so even the native drawing projects them.\n */\nconst sideCamera = robotCamera(NATIVE_VIEW)\n\n/** A point in the animal's own frame: nose-ward, up, and off the centre plane. */\ninterface Solid {\n  forward: number\n  up: number\n  across: number\n}\n\ninterface HorseLimb {\n  id: GaitLegId\n  side: \"left\" | \"right\"\n  fore: boolean\n  /** Share of the body's weight this limb is carrying, 0–1. */\n  load: number\n  contact: boolean\n  /** Shoulder, or hip. */\n  root: Vec2\n  /** Elbow, or stifle. */\n  mid: Vec2\n  /** Knee, or hock — the top of the cannon. */\n  knee: Vec2\n  /** The sprung joint: its height over the hoof is `fetlockSink(load)` and\n   *  nothing else. */\n  fetlock: Vec2\n  hoof: Vec2\n}\n\nexport interface RobotHorseProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /** Where the camera stands. One animal, four projections. */\n  view?: RobotView\n  /** What it does when `phase` is not supplied. */\n  behavior?: HorseBehavior\n  /** Footfall pattern, overriding the one the behavior picked. `pace` is only reachable here. */\n  gait?: EquineGait\n  /** Which foreleg lands last. Only the canter and the gallop have a lead. */\n  lead?: GaitLead\n  /** Controlled cycle fraction. Supplying it stops the internal clock. */\n  phase?: number\n  /** Strides per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  /** Seconds of offset, so a string of them breaks step. */\n  offset?: number\n  /** Back curvature, −1 hollowed to 1 roached. Omit and the behavior sets it. */\n  arch?: number\n  /** Leg fold, 0 standing tall to 1 dropped. Omit and the behavior decides. */\n  crouch?: number\n  /** Scripted neck carriage, −1 head to the floor to 1 head up. What the balance moves. */\n  neck?: number\n  /** How much of the carriage the forehand's load takes, 0 scripted to 1 fully derived. */\n  balance?: number\n  /** Tail carriage, −1 clamped down to 1 flagged up. Omit and the behavior sets it. */\n  tail?: number\n  /** Ears, −1 pinned back to 1 pricked forward. Omit and they answer the pointer. */\n  ears?: number\n  /** Head and eye aim, −1..1. Omit and it follows the pointer. */\n  gaze?: number\n  /** Drag across to scrub the stride; arrows step it. The head tracks the pointer. */\n  interactive?: boolean\n  onPhaseChange?: (phase: number) => void\n  size?: RobotSize | number\n  variant?: RobotVariant\n  showGround?: boolean\n  /** Mark the hooves carrying weight. */\n  showContacts?: boolean\n  label?: string\n}\n\nfunction RobotHorse({\n  behavior = \"walk\", gait, lead = \"right\", phase, view = NATIVE_VIEW,\n  speed = 0.6, animate = true, paused = false, offset = 0,\n  arch, crouch, neck, balance, tail, ears, gaze,\n  interactive = true, onPhaseChange,\n  size = \"md\", variant = \"solid\", showGround = true, showContacts = false, label,\n  color, accent, metal, dark, glow, grid, palette: paletteOverride, className, style,\n  role, tabIndex, onKeyDown, onBlur, ...props\n}: RobotHorseProps) {\n  const controlled = phase !== undefined\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const [scrubbed, setScrubbed] = React.useState<number | null>(null)\n\n  const clock = useRobotClock({\n    speed: 1,\n    animate: animate && !controlled && scrubbed === null && behavior !== \"static\",\n    paused,\n    phase: offset,\n  })\n  const scripted = horseBehaviorPose(behavior, clock)\n  const beat = controlled\n    ? finiteClamp(phase, -1e6, 1e6, 0)\n    : (scrubbed ?? clock * (Number.isFinite(speed) ? speed : 0))\n  const cycle = wrap(beat)\n  const stance = scripted.stance(cycle)\n\n  const apply = React.useCallback(\n    (next: number) => {\n      const wrapped = wrap(next)\n      setScrubbed(wrapped)\n      onPhaseChange?.(wrapped)\n    },\n    [onPhaseChange],\n  )\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    // The width of the box is one whole stride, so a person can walk it\n    // through the footfalls one at a time.\n    onDrag: React.useCallback((unit: Vec2) => apply(unit.x), [apply]),\n    onDragEnd: React.useCallback(() => {}, []),\n  })\n\n  const pointer = usePointerTarget(svgRef, {\n    enabled: interactive && !paused,\n    within: \"element\",\n    persist: true,\n    toWorld: React.useCallback((unit: Vec2) => ({\n      x: clamp((unit.x - 0.5) * 2.2, -1, 1),\n      y: clamp((0.5 - unit.y) * 2, -1, 1),\n    }), []),\n  })\n\n  /* ---- the gait: who is down, and what each of them is carrying ---------- */\n\n  const walking = gait ?? scripted.gait\n  const pose = solveGait({\n    gait: walking,\n    phase: cycle,\n    lead,\n    stride: scripted.stride,\n    lift: scripted.lift,\n  })\n  // What the forehand is carrying right now, against what it carries standing\n  // square. That difference is the whole of the nod.\n  const foreLoad = pose.legs.reduce((sum, leg) => (leg.fore ? sum + leg.load : sum), 0)\n  const nod = NOD * (foreLoad - pose.forehand)\n\n  /* ---- the back, anchored at the withers --------------------------------- */\n\n  const bow = finiteClamp(arch ?? stance.arch, -1, 1, stance.arch)\n  const fold = finiteClamp(crouch ?? stance.crouch, 0, 1, stance.crouch)\n  const rise = clamp(stance.altitude, 0, 1) * RISE\n  // A horse holds a topline: the same restrained scaling the dog and the fox\n  // use, positive roaching the back and negative hollowing it.\n  const curvature = bow * 0.2\n  // Half the solver's arc, run back, puts the crown in the middle with both\n  // ends level instead of dropping one of them.\n  const tilt = -(curvature * spineLimits.turn) / 2\n  const back = tiltPose(\n    solveSpine({\n      segments: 6,\n      length: TRUNK,\n      phase: cycle,\n      amplitude: scripted.flex,\n      waves: 0.7,\n      taper: 0.35,\n      turn: curvature,\n    }),\n    tilt,\n  )\n  // The withers are the anchor: the solved chain is re-hung on its first joint,\n  // so the croup is what the arch and the stride move.\n  const withersHeight = lerp(WITHERS, WITHERS - 12, fold) + rise\n  const nose = back.joints[0].position\n  const spinePoint = (index: number): Vec2 => ({\n    x: back.joints[index].position.x - nose.x + 16,\n    y: back.joints[index].position.y - nose.y + withersHeight,\n  })\n  const last = back.joints.length - 1\n  const withersAt = spinePoint(0)\n  const croup = spinePoint(last)\n\n  /** A point `down` world units below the back line at spine joint `index`. */\n  const underBack = (index: number, down: number): Vec2 => {\n    const normal = toRadians(back.joints[index].angle + 90)\n    const at = spinePoint(index)\n    return { x: at.x - Math.cos(normal) * down, y: at.y - Math.sin(normal) * down }\n  }\n  const shoulder = underBack(1, 9)\n  const hip = underBack(5, 8)\n\n  /* ---- four limbs, each with one joint nobody sets ------------------------ */\n\n  const metaAngle = lerp(22, 40, fold)\n  const limbs: HorseLimb[] = pose.legs.map((leg) => solveLimb(leg, shoulder, hip, metaAngle, rise))\n\n  /* ---- the neck: solved, and carried by the forehand's load --------------- */\n\n  const scriptedNeck = finiteClamp(neck ?? stance.neck, -1, 1, stance.neck)\n  const weight = finiteClamp(balance ?? scripted.balance, 0, 1, scripted.balance)\n  const carriage = clamp(scriptedNeck + nod * weight, -1, 1)\n  const crestAngle = back.joints[0].angle\n  const nape = alongBody(withersAt, crestAngle, 5, 8)\n  // The target is set in polar terms, at very nearly the chain's full reach:\n  // a two-link neck asked for a point well inside its own reach folds into a\n  // loop rather than arching, and an arched crest is the whole silhouette.\n  const carried = (carriage + 1) / 2\n  const reach = alongBody(\n    nape,\n    crestAngle + lerp(NECK_ANGLE.down, NECK_ANGLE.up, carried),\n    lerp(NECK_SPAN.down, NECK_SPAN.up, carried),\n    0,\n  )\n  const [, crest, poll] = solveChain2(nape, reach, [...NECK], { bend: \"down\" })\n  const aim = finiteClamp(gaze ?? pointer.target?.x ?? scripted.gaze, -1, 1, 0)\n  // The head hangs off the poll: a horse carries its face well below the line\n  // of its own neck, which is what the offset is.\n  const headTilt = toDegrees(Math.atan2(poll.y - crest.y, poll.x - crest.x)) + aim * 7 - 24\n\n  /* ---- mane and tail ----------------------------------------------------- */\n\n  // The mane is a ribbon on the neck's own line, so it follows the solved chain\n  // rather than having to be kept in step with it.\n  const crestLine = sampleRibbon([nape, crest, lerp2(crest, poll, 0.84)], 9)\n  const maneJoints = crestLine.map((point, index) => {\n    const s = index / (crestLine.length - 1)\n    const before = crestLine[Math.max(0, index - 1)]\n    const after = crestLine[Math.min(crestLine.length - 1, index + 1)]\n    // The crest's own outward normal, so the mane sits on the top line of the\n    // neck at any carriage instead of always standing straight up.\n    const normal = normalize2({ x: -(after.y - before.y), y: after.x - before.x }, { x: 0, y: 1 })\n    const streamed = scripted.mane * Math.sin(Math.PI * 2 * (cycle * 1.5 - s * 0.6)) * (0.3 + s * 0.7)\n    // Measured from the crest line, so the ribbon starts at the neck's own\n    // surface: the inner edge is the neck, the outer edge is the hair.\n    const out = lerp(6.5, 2.6, s) + streamed * 1.8\n    return { x: point.x + normal.x * out, y: point.y + normal.y * out }\n  })\n\n  const tailCarriage = finiteClamp(tail ?? scripted.tail, -1, 1, scripted.tail)\n  const skirt = solveSpine({\n    segments: 7,\n    length: TAIL,\n    phase: cycle * 1.4,\n    amplitude: clamp(scripted.mane * 0.8, 0, 1) * 0.5,\n    waves: 0.9,\n    taper: 1,\n    turn: lerp(0.42, -0.12, (tailCarriage + 1) / 2),\n  })\n  // A tail hangs by default and has to be carried to come up, so the two\n  // halves of the range are not the same size.\n  const tailTurn = back.joints[last].angle + (tailCarriage >= 0\n    ? lerp(TAIL_SET.rest, TAIL_SET.flagged, tailCarriage)\n    : lerp(TAIL_SET.rest, TAIL_SET.clamped, -tailCarriage))\n  const tailJoints = skirt.joints.map((joint) => {\n    const point = rotate2(joint.position, toRadians(tailTurn))\n    // A tail hangs; it does not go through the floor.\n    return { x: point.x + croup.x, y: Math.max(1.5, point.y + croup.y) }\n  })\n\n  /* ---- ears -------------------------------------------------------------- */\n\n  const earAim = finiteClamp(ears ?? (pointer.target ? 1 : scripted.ears), -1, 1, 0)\n  // Pricked forward, or rotated back and flattened onto the poll.\n  const earTip = lerp(-34, 16, (earAim + 1) / 2)\n  const earPan = lerp(26, 4, (earAim + 1) / 2)\n\n  /* ---- paint ------------------------------------------------------------- */\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\n  const camera = robotCamera(view)\n  const offAxis = view !== NATIVE_VIEW\n  const fit = fits[view] ?? 1\n  const face = aboutPoint(camera.wall(0, 90), ORIGIN, GROUND, fit)\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 animal's own frame, `across` units off the centre plane. */\n  const at = (p: Vec2, across = 0) => camera.project(across, p.y, -p.x)\n  /** The same for a point that already knows how far off the plane it is. */\n  const solid = (p: Solid) => camera.project(p.across, p.up, -p.forward)\n  /** A three-dimensional point in the flat side-elevation drawing. */\n  const flat = (p: Solid): Vec2 => {\n    const screen = sideCamera.project(p.across, p.up, -p.forward)\n    return { x: screen.x, y: -screen.y }\n  }\n\n  const readout = Math.round(cycle * 100)\n  const state = behavior === \"graze\"\n    ? \"grazing\"\n    : walking === \"halt\"\n      ? \"standing square\"\n      : `at a ${walking}`\n  const gaitPhrase = pose.beats > 0\n    ? `${beatNames[pose.beats] ?? `${pose.beats}-beat`} ${walking}${pose.leadLeg ? ` on the ${pose.lead} lead` : \"\"}, ${readout} percent through its stride`\n    : \"four feet down\"\n\n  /** One ear's outline, in the animal's own three-space frame. */\n  function earOutline(across: number): Solid[] {\n    const pan = toRadians(across > 0 ? earPan : -earPan)\n    const t = toRadians(earTip)\n    // An ear is a funnel, not a plate: its outline stands in the plane that\n    // contains the vertical and the direction it faces, so panning it round\n    // foreshortens the triangle instead of leaving it flat to the camera.\n    const side = { forward: Math.cos(pan), up: 0, across: Math.sin(pan) }\n    const up = {\n      forward: -Math.cos(pan) * Math.sin(t),\n      up: Math.cos(t),\n      across: -Math.sin(pan) * Math.sin(t),\n    }\n    const base = alongBody(poll, headTilt, -1.5, 4.5)\n    const point = (along: number, out: number): Solid => ({\n      forward: base.x + up.forward * along + side.forward * out,\n      up: base.y + up.up * along + side.up * out,\n      across: across + up.across * along + side.across * out,\n    })\n    return [point(0, -EAR_HALF), point(EAR_HEIGHT, EAR_HALF * 0.18), point(0, EAR_HALF)]\n  }\n\n  /** One limb, in the animal's own y-up frame. */\n  function limbDrawing(limb: HorseLimb) {\n    const far = limb.side === \"right\"\n    const shift = far ? -7 : 0\n    const move = (p: Vec2): Vec2 => ({ x: p.x + shift, y: p.y })\n    return (\n      <g\n        key={limb.id}\n        data-leg={limb.id}\n        data-load={px(limb.load)}\n        opacity={far ? 0.5 : 1}\n      >\n        <path d={capsulePath(move(limb.root), move(limb.mid), limb.fore ? 5.4 : 6.6)} {...shell} />\n        <path d={capsulePath(move(limb.mid), move(limb.knee), limb.fore ? 3.8 : 4.4)} {...machined} />\n        {/* The cannon, then the pastern, which is the sprung one. */}\n        <path d={capsulePath(move(limb.knee), move(limb.fetlock), 2.4)} {...cast} />\n        <path data-pastern={limb.id} d={capsulePath(move(limb.fetlock), move(limb.hoof), 2.1)} {...machined} />\n        {/* The hoof: a wedge on the floor, toe forward. */}\n        <path\n          d={`M ${px(move(limb.hoof).x - 3.4)} ${px(move(limb.hoof).y + 1.2)} L ${px(move(limb.hoof).x + 3.6)} ${px(move(limb.hoof).y + 1.6)} L ${px(move(limb.hoof).x + 3.2)} ${px(move(limb.hoof).y - 2.6)} L ${px(move(limb.hoof).x - 3)} ${px(move(limb.hoof).y - 2.6)} Z`}\n          {...cast}\n        />\n        <circle data-hoof={limb.id} cx={px(move(limb.hoof).x)} cy={px(move(limb.hoof).y)} r={0.01} fill=\"none\" />\n        <circle\n          data-joint={`${limb.id}-${limb.fore ? \"elbow\" : \"stifle\"}`}\n          cx={px(move(limb.mid).x)} cy={px(move(limb.mid).y)} r={3.4} {...cast}\n        />\n        <circle cx={px(move(limb.knee).x)} cy={px(move(limb.knee).y)} r={2.6} {...cast} />\n        {/* The joint no one sets: it sits where the load puts it, and lights\n            in proportion to what it is carrying. */}\n        <circle data-fetlock={limb.id} cx={px(move(limb.fetlock).x)} cy={px(move(limb.fetlock).y)} r={2.6} {...cast} />\n        {limb.load > 0 && (\n          <circle\n            cx={px(move(limb.fetlock).x)} cy={px(move(limb.fetlock).y)} r={px(0.7 + limb.load * 1.2)}\n            fill={palette.accent}\n          />\n        )}\n        {showContacts && limb.contact && (\n          <ellipse data-contact cx={px(move(limb.hoof).x)} cy={1.2} rx={5} ry={1.1} fill={palette.accent} opacity={0.6} />\n        )}\n      </g>\n    )\n  }\n\n  return (\n    <svg\n      ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={`Robot horse, ${state}, ${gaitPhrase}, ${viewNames[view] ?? viewNames.profile}`}\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? 100 : undefined}\n      aria-valuenow={interactive ? readout : undefined}\n      aria-valuetext={interactive ? `${readout} percent through the stride` : undefined}\n      tabIndex={tabIndex ?? (interactive ? 0 : undefined)}\n      viewBox={`0 0 ${BOX.width} ${BOX.height}`}\n      width={width}\n      height={px((width * BOX.height) / BOX.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 step = event.shiftKey ? 0.15 : 0.05\n        if (event.key === \"ArrowRight\" || event.key === \"ArrowUp\") apply(cycle + step)\n        else if (event.key === \"ArrowLeft\" || event.key === \"ArrowDown\") apply(cycle - step)\n        else if (event.key === \"Home\") apply(0)\n        else if (event.key === \"End\") setScrubbed(null)\n        else return\n        event.preventDefault()\n      }}\n      onBlur={(event) => {\n        onBlur?.(event)\n        if (!dragging && !controlled) setScrubbed(null)\n      }}\n      {...props}\n    >\n      {variant === \"blueprint\" && (\n        <g fill=\"none\" stroke={palette.grid} strokeWidth={0.5} opacity={0.4}>\n          <path d={`M 12 ${GROUND} H ${BOX.width - 12} M ${ORIGIN} 14 V ${GROUND + 16}`} strokeDasharray=\"2 3\" />\n          {/* The stride, marked out under the machine: one box is one cycle. */}\n          <path\n            data-stride\n            d={`M 20 ${GROUND + 12} H ${px(20 + (BOX.width - 40) * cycle)}`}\n            strokeDasharray=\"1 2\"\n          />\n        </g>\n      )}\n      {showGround && (\n        <g data-ground>\n          <path d={`M 14 ${GROUND} H ${BOX.width - 14}`} stroke={palette.grid} strokeWidth={0.8} fill=\"none\" />\n          <ellipse\n            cx={ORIGIN}\n            cy={px(GROUND + 3)}\n            rx={px(54 - rise * 0.8)}\n            ry={px(4.6 - rise * 0.1)}\n            fill={palette.dark}\n            opacity={px(0.16 - rise * 0.004)}\n          />\n        </g>\n      )}\n\n      {offAxis && (\n        <g data-solids transform={`translate(${ORIGIN} ${GROUND}) scale(${px(fit)})`}>\n          {/* The barrel, one extruded footprint per spine segment, so the\n              topline survives the projection instead of flattening to a box. */}\n          {back.joints.slice(0, -1).map((joint, index) => {\n            const a = spinePoint(index)\n            const b = spinePoint(index + 1)\n            const midX = (a.x + b.x) / 2\n            const midY = (a.y + b.y) / 2\n            const halfLength = Math.hypot(b.x - a.x, b.y - a.y) / 2 + 1.5\n            const footprint = roundedFootprint(HALF_TRACK, halfLength, 5, 4).map((p) => ({ x: p.x, y: p.y - midX }))\n            return <path key={index} d={extrudedPath(footprint, camera, midY + 10, midY - 11)} {...shell} />\n          })}\n          {/* Each limb on its own side of the centre plane, so a plan or a\n              front elevation shows the real splay of the stride rather than\n              four legs stacked in one line. */}\n          {limbs.map((limb) => {\n            const across = limb.side === \"left\" ? HALF_TRACK : -HALF_TRACK\n            return (\n              <g key={limb.id}>\n                <path d={capsulePath(at(limb.root, across * 0.6), at(limb.mid, across), limb.fore ? 5.4 : 6.6)} {...shell} />\n                <path d={capsulePath(at(limb.mid, across), at(limb.knee, across), limb.fore ? 3.8 : 4.4)} {...machined} />\n                <path d={capsulePath(at(limb.knee, across), at(limb.fetlock, across), 2.4)} {...cast} />\n                <path d={capsulePath(at(limb.fetlock, across), at(limb.hoof, across), 2.1)} {...machined} />\n              </g>\n            )\n          })}\n          {/* The neck and the skull as solids, and the tail as a tapering tube. */}\n          <path d={capsulePath(at(nape), at(crest), 9)} {...machined} />\n          <path d={capsulePath(at(crest), at(poll), 6.2)} {...machined} />\n          <path\n            d={extrudedPath(\n              roundedFootprint(5.5, 15, 5, 5).map((p) => ({ x: p.x, y: p.y - poll.x })),\n              camera,\n              poll.y + 5,\n              poll.y - 6,\n            )}\n            {...shell}\n          />\n          {tailJoints.slice(0, -1).map((joint, index) => (\n            <path\n              key={index}\n              d={capsulePath(at(joint), at(tailJoints[index + 1]), px(4 * (1 - (index / (tailJoints.length - 1)) ** 1.6) + 1.2))}\n              {...machined}\n            />\n          ))}\n        </g>\n      )}\n\n      <Frame {...frame}>\n        {/* The drawing works in the animal's own frame: x forward, y up. */}\n        <g data-horse data-view={view} transform={`translate(${ORIGIN} ${GROUND}) scale(1 -1)`}>\n          {limbs.filter((limb) => limb.side === \"right\").map(limbDrawing)}\n\n          <g data-tail>\n            {tailJoints.slice(0, -1).map((joint, index) => (\n              <path\n                key={index}\n                d={capsulePath(joint, tailJoints[index + 1], px(4 * (1 - (index / (tailJoints.length - 1)) ** 1.6) + 1.2))}\n                {...machined}\n              />\n            ))}\n            <circle cx={px(croup.x)} cy={px(croup.y)} r={3.4} {...cast} />\n          </g>\n\n          <g data-trunk>\n            {/* The barrel is the solver's output: level topline, deep girth, a\n                croup that comes back down over the hind legs. */}\n            {([0, last] as const).map((index) => {\n              const joint = back.joints[index]\n              const place = spinePoint(index)\n              const normal = toRadians(joint.angle + 90)\n              const radius = (backline(joint.s) + girth(joint.s)) / 2\n              const nudge = (backline(joint.s) - girth(joint.s)) / 2\n              return (\n                <circle\n                  key={index}\n                  cx={px(place.x + Math.cos(normal) * nudge)}\n                  cy={px(place.y + Math.sin(normal) * nudge)}\n                  r={px(radius)}\n                  {...shell}\n                />\n              )\n            })}\n            <path data-spine d={bodyOutline(back, spinePoint, backline, girth)} {...shell} />\n            {/* Two seams down the flank, and nothing more. */}\n            <g fill=\"none\" stroke={palette.dark} strokeWidth={0.7} opacity={0.24}>\n              <path d={offsetLine(back, spinePoint, (t) => 4.5 - 1.6 * Math.sin(Math.PI * t))} />\n              <path d={offsetLine(back, spinePoint, (t) => -(8 - 3 * Math.sin(Math.PI * t)))} />\n            </g>\n          </g>\n\n          {/* The neck: two solved links, and the mane riding the crest. */}\n          <path data-neck d={capsulePath(nape, crest, 9)} {...machined} />\n          <path d={capsulePath(crest, poll, 6.2)} {...machined} />\n          <path\n            data-mane\n            d={`${maneJoints.map((joint, index) => `${index ? \"L\" : \"M\"} ${px(joint.x)} ${px(joint.y)}`).join(\" \")} ${[...crestLine].reverse().map((joint) => `L ${px(joint.x)} ${px(joint.y)}`).join(\" \")} Z`}\n            {...cast}\n          />\n\n          <g data-ears>\n            {([-EAR_ACROSS, EAR_ACROSS] as const).map((across) => (\n              <path\n                key={across}\n                data-ear={across > 0 ? \"left\" : \"right\"}\n                d={polygon(earOutline(across).map(view === NATIVE_VIEW ? flat : solid))}\n                opacity={across > 0 ? 1 : 0.62}\n                {...shell}\n              />\n            ))}\n          </g>\n\n          <g data-head transform={`translate(${px(poll.x)} ${px(poll.y)}) rotate(${px(-headTilt)})`}>\n            {/* The poll is the top-rear corner of the skull, not its centre:\n                a head hangs off the end of the neck. */}\n            <g transform=\"translate(1 -3) scale(0.98)\">\n            {/* A long head: deep at the jowl, straight down the face, and\n                nearly as long again in front of the eye. */}\n            {/* One head: jowl, face and muzzle in a single outline, because a\n                horse's head has no break in it. The dark band is the muzzle\n                itself, which is the only part that is another colour. */}\n            <path\n              d=\"M -8 -9 Q -12 7 -1.5 9.5 Q 7 9.5 11 5.5 Q 18 4.6 26 3.4 Q 29.5 2.6 29.5 -0.6 Q 29.5 -4.4 25.5 -5.4 Q 17 -7.4 10 -7.4 L 9.5 -6 Q 4 -11.5 -3 -11.5 Q -7.5 -11.5 -8 -9 Z\"\n              {...shell}\n            />\n            <path d=\"M 22 -6.4 Q 29.5 -5 29.5 -0.6 Q 29.5 2.6 25.8 3.5 Q 22.5 4 21.5 3.8 Q 24 0.6 22 -6.4 Z\" {...cast} />\n            <circle cx={25.4} cy={-1.4} r={1.7} fill={palette.metal} />\n            {/* The jowl and the cheekbone, which are what give the head its edge. */}\n            <path d=\"M -6.5 -1.5 Q 1.5 -4 7 -0.5 Q 1.5 2.5 -5.5 1.5 Z\" fill={palette.dark} opacity={0.18} stroke=\"none\" />\n            <path d=\"M 10.5 -6.6 L 11 5.4\" fill=\"none\" stroke={palette.dark} strokeWidth={0.6} opacity={0.3} />\n            <g data-eyes>\n              <g transform=\"translate(-3.4 -5)\" opacity={0.5}>\n                <circle r={2.4} {...cast} />\n                <circle cx={px(0.8 + aim * 0.9)} r={1.1} fill={palette.accent} />\n              </g>\n              <g transform=\"translate(1.8 -5.6)\">\n                <circle r={3.1} {...cast} />\n                <circle cx={px(1 + aim * 1.2)} r={1.5} fill={palette.accent} />\n              </g>\n            </g>\n            </g>\n          </g>\n\n          {limbs.filter((limb) => limb.side === \"left\").map(limbDrawing)}\n\n          <g data-joints>\n            <circle data-joint=\"withers\" cx={px(withersAt.x)} cy={px(withersAt.y)} r={3} {...cast} />\n            <circle data-joint=\"hip\" cx={px(hip.x)} cy={px(hip.y)} r={4.6} {...cast} />\n            <circle cx={px(hip.x)} cy={px(hip.y)} r={1.8} fill={palette.metal} />\n            <circle data-joint=\"poll\" cx={px(poll.x)} cy={px(poll.y)} r={2.6} {...cast} />\n          </g>\n        </g>\n      </Frame>\n\n      {label && (\n        <text x={BOX.width / 2} y={BOX.height - 6} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\" fontSize={6} fill={palette.foreground}>\n          {label}\n        </text>\n      )}\n    </svg>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* geometry                                                                    */\n/* -------------------------------------------------------------------------- */\n\n/**\n * One limb, built from the hoof upwards, which is the only order the sprung\n * fetlock can be built in: the pastern's angle comes from the load, so the\n * fetlock's place is known before anything above it is, and the chain above is\n * then solved to it.\n */\nfunction solveLimb(\n  leg: GaitLeg,\n  shoulder: Vec2,\n  hip: Vec2,\n  metaAngle: number,\n  rise: number,\n): HorseLimb {\n  const root = leg.fore ? shoulder : hip\n  const hoof: Vec2 = {\n    x: (leg.fore ? shoulder.x - 1 : hip.x + 3) + leg.foot.x,\n    y: leg.foot.y + rise,\n  }\n  // The one angle nobody sets: a loaded pastern flattens toward the floor and\n  // a free one stands back up.\n  const pastern = toRadians(PASTERN_REST - fetlockSink(leg.load))\n  const fetlock: Vec2 = {\n    x: hoof.x - Math.cos(pastern) * PASTERN,\n    y: hoof.y + Math.sin(pastern) * PASTERN,\n  }\n  if (leg.fore) {\n    // The knee carries the cannon straight up off the fetlock, and the humerus\n    // and radius solve to it.\n    const knee: Vec2 = { x: fetlock.x + 1.5, y: fetlock.y + FORE_CANNON }\n    const [, elbow] = solveChain2(root, knee, [...FORE], { bend: \"down\" })\n    return { id: leg.id, side: leg.side, fore: true, load: leg.load, contact: leg.contact && rise < 0.5, root, mid: elbow, knee, fetlock, hoof }\n  }\n  // The hock is where the free parameter of a three-link hind limb is spent:\n  // the metatarsus stands at an angle that opens with the crouch, and the femur\n  // and tibia solve to it.\n  const hock: Vec2 = {\n    x: fetlock.x - Math.sin(toRadians(metaAngle)) * HIND_CANNON,\n    y: fetlock.y + Math.cos(toRadians(metaAngle)) * HIND_CANNON,\n  }\n  const [, stifle] = solveChain2(root, hock, [...HIND], { bend: \"up\" })\n  return { id: leg.id, side: leg.side, fore: false, load: leg.load, contact: leg.contact && rise < 0.5, root, mid: stifle, knee: hock, fetlock, hoof }\n}\n\n/** The barrel, withers to croup: level topline, deep girth, a tucked flank. */\nconst backline = (s: number) => 8.5 + 3.5 * s * s\nconst girth = (s: number) =>\n  s < 0.35 ? lerp(15.5, 11.5, s / 0.35) : lerp(11.5, 16, (s - 0.35) / 0.65)\n\n/** The same pose turned bodily about its nose, tangents and all. */\nfunction tiltPose(pose: SpinePose, degrees: number): SpinePose {\n  if (!degrees) return pose\n  const radians = toRadians(degrees)\n  const joints = pose.joints.map((joint) => ({\n    ...joint,\n    position: rotate2(joint.position, radians),\n    angle: joint.angle + degrees,\n  }))\n  return { ...pose, joints, head: joints[0], tail: joints[joints.length - 1] }\n}\n\n/** A point `forward` along a body axis and `up` its normal. */\nfunction alongBody(origin: Vec2, degrees: number, forward: number, up: number): Vec2 {\n  const a = toRadians(degrees)\n  return {\n    x: origin.x + Math.cos(a) * forward - Math.sin(a) * up,\n    y: origin.y + Math.sin(a) * forward + Math.cos(a) * up,\n  }\n}\n\n/** A polyline resampled to `count` evenly spaced points along its own length. */\nfunction sampleRibbon(points: Vec2[], count: number): Vec2[] {\n  const spans = points.slice(0, -1).map((point, index) => Math.hypot(points[index + 1].x - point.x, points[index + 1].y - point.y))\n  const total = spans.reduce((sum, span) => sum + span, 0)\n  if (total < 1e-6) return Array.from({ length: count }, () => points[0])\n  return Array.from({ length: count }, (_, step) => {\n    let walk = (step / (count - 1)) * total\n    for (let index = 0; index < spans.length; index += 1) {\n      if (walk <= spans[index] || index === spans.length - 1) {\n        const t = spans[index] < 1e-6 ? 0 : clamp(walk / spans[index], 0, 1)\n        return {\n          x: lerp(points[index].x, points[index + 1].x, t),\n          y: lerp(points[index].y, points[index + 1].y, t),\n        }\n      }\n      walk -= spans[index]\n    }\n    return points[points.length - 1]\n  })\n}\n\n/** The spine's own line, offset along each joint's normal and left open. */\nfunction offsetLine(pose: SpinePose, place: (index: number) => Vec2, width: (s: number) => number) {\n  return pose.joints\n    .map((joint, index) => {\n      const normal = toRadians(joint.angle + 90)\n      const at = place(index)\n      const w = width(joint.s)\n      return `${index ? \"L\" : \"M\"} ${px(at.x + Math.cos(normal) * w)} ${px(at.y + Math.sin(normal) * w)}`\n    })\n    .join(\" \")\n}\n\n/** Joints offset by a different amount each side, closed into one path. */\nfunction bodyOutline(\n  pose: SpinePose,\n  place: (index: number) => Vec2,\n  top: (s: number) => number,\n  under: (s: number) => number,\n) {\n  const above: string[] = []\n  const below: string[] = []\n  pose.joints.forEach((joint, index) => {\n    const normal = toRadians(joint.angle + 90)\n    const nx = Math.cos(normal)\n    const ny = Math.sin(normal)\n    const at = place(index)\n    const a = top(joint.s)\n    const b = under(joint.s)\n    above.push(`${above.length ? \"L\" : \"M\"} ${px(at.x + nx * a)} ${px(at.y + ny * a)}`)\n    below.unshift(`L ${px(at.x - nx * b)} ${px(at.y - ny * b)}`)\n  })\n  return [...above, ...below, \"Z\"].join(\" \")\n}\n\nconst polygon = (points: Vec2[]) =>\n  `${points.map((p, index) => `${index ? \"L\" : \"M\"} ${px(p.x)} ${px(p.y)}`).join(\" \")} Z`\n\n/** A control that has to survive a consumer handing it `NaN`. */\nconst finiteClamp = (value: number | undefined, min: number, max: number, fallback: number) =>\n  Number.isFinite(value) ? clamp(value as number, min, max) : fallback\n\nconst wrap = (value: number) => (Number.isFinite(value) ? ((value % 1) + 1) % 1 : 0)\n\n/* -------------------------------------------------------------------------- */\n/* behaviour                                                                   */\n/* -------------------------------------------------------------------------- */\n\nexport interface HorseStance {\n  /** Back curvature, −1 hollow to 1 roached. */\n  arch: number\n  /** Leg fold, 0 tall to 1 dropped. */\n  crouch: number\n  /** Scripted neck carriage, −1 head to the floor to 1 head up. */\n  neck: number\n  /** Height off the floor through a suspension, 0–1. */\n  altitude: number\n}\n\nexport interface HorsePose {\n  /** The footfall pattern this behaviour runs. */\n  gait: EquineGait\n  gaze: number\n  ears: number\n  tail: number\n  /** How much of the neck's carriage the forehand's load takes, 0–1. */\n  balance: number\n  /** Normalized foot travel, 0–1. */\n  stride: number\n  /** Normalized swing height, 0–1. */\n  lift: number\n  /** Mane and tail wave amplitude, 0–1. */\n  mane: number\n  /** Spine wave amplitude, 0–1. A horse holds a topline, so it is nearly spent. */\n  flex: number\n  stance: (cycle: number) => HorseStance\n}\n\n/**\n * What it does with no timeline on it. Pure in the clock, so the tests sample\n * it directly rather than faking animation frames.\n */\nexport function horseBehaviorPose(behavior: HorseBehavior, clock: number): HorsePose {\n  const time = Number.isFinite(clock) ? clock : 0\n  switch (behavior) {\n    // Two beats, diagonal pairs, with a suspension between each. The back is\n    // level and there is almost nothing for the nod to do, which is exactly\n    // what a trot looks like.\n    case \"trot\":\n      return {\n        gait: \"trot\",\n        gaze: 0.12 * Math.sin(time * 0.7),\n        ears: 0.8,\n        tail: 0.35,\n        balance: 0.25,\n        stride: 0.72,\n        lift: 0.6,\n        mane: 0.35,\n        flex: 0.04,\n        stance: (cycle) => ({\n          arch: 0.02,\n          crouch: 0.16,\n          neck: 0.66,\n          // Two suspensions a stride: one after each diagonal leaves.\n          altitude: 0.1 * Math.max(0, Math.sin(2 * Math.PI * (2 * wrap(cycle) + 0.22))),\n        }),\n      }\n    // Three beats on a lead, and the one gait where the whole body rises and\n    // falls once a stride.\n    case \"canter\":\n      return {\n        gait: \"canter\",\n        gaze: 0.1 * Math.sin(time * 0.8),\n        ears: 0.85,\n        tail: 0.5,\n        balance: 0.45,\n        stride: 0.85,\n        lift: 0.7,\n        mane: 0.6,\n        flex: 0.08,\n        stance: (cycle) => {\n          const t = wrap(cycle)\n          return {\n            arch: 0.12 * Math.sin(2 * Math.PI * t),\n            crouch: 0.14,\n            neck: 0.62,\n            altitude: 0.42 * Math.max(0, Math.sin(Math.PI * clamp((t - 0.72) / 0.3, 0, 1))),\n          }\n        },\n      }\n    // Four beats, the longest stride, and everything streaming.\n    case \"gallop\":\n      return {\n        gait: \"gallop\",\n        gaze: 0.06 * Math.sin(time * 0.9),\n        ears: 0.55,\n        tail: 0.8,\n        balance: 0.55,\n        stride: 1,\n        lift: 0.85,\n        mane: 1,\n        flex: 0.12,\n        stance: (cycle) => {\n          const t = wrap(cycle)\n          return {\n            arch: 0.2 * Math.sin(2 * Math.PI * t),\n            crouch: 0.1,\n            neck: 0.46,\n            altitude: 0.6 * Math.max(0, Math.sin(Math.PI * clamp((t - 0.78) / 0.26, 0, 1))),\n          }\n        },\n      }\n    // Halted with the head right down, and the odd shift of weight.\n    case \"graze\":\n      return {\n        gait: \"halt\",\n        gaze: 0.08 * Math.sin(time * 0.4),\n        ears: -0.2,\n        tail: 0.15,\n        balance: 0,\n        stride: 0,\n        lift: 0,\n        mane: 0.08,\n        flex: 0.02,\n        stance: () => ({\n          arch: -0.12,\n          crouch: 0.1,\n          neck: -0.95,\n          altitude: 0,\n        }),\n      }\n    case \"static\":\n      return {\n        gait: \"halt\",\n        gaze: 0,\n        ears: 0.5,\n        tail: 0.2,\n        balance: 0,\n        stride: 0,\n        lift: 0,\n        mane: 0,\n        flex: 0,\n        stance: () => ({ arch: 0, crouch: 0.12, neck: 0.68, altitude: 0 }),\n      }\n    // The signature: four beats in a lateral sequence, never off the floor,\n    // and the neck nodding once a stride because the forehand loads and\n    // unloads once a stride. Nobody scripts that nod.\n    default:\n      return {\n        gait: \"walk\",\n        gaze: 0.16 * Math.sin(time * 0.5),\n        ears: 0.7,\n        tail: 0.25,\n        balance: 1,\n        stride: 0.55,\n        lift: 0.45,\n        mane: 0.2,\n        flex: 0.03,\n        stance: () => ({ arch: 0, crouch: 0.14, neck: 0.62, altitude: 0 }),\n      }\n  }\n}\n\nexport { RobotHorse }\n",
      "type": "registry:ui",
      "target": "@ui/robot-horse.tsx"
    }
  ],
  "categories": [
    "robotics",
    "animals"
  ],
  "type": "registry:ui"
}