{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "leg-press",
  "title": "Leg press",
  "description": "A sled on inclined rails: only the component of the load along the rails resists, so the rail angle is the resistance and the frame decides the weight.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/use-robot-motion.json",
    "https://robocn.dev/r/gym-geometry.json"
  ],
  "files": [
    {
      "path": "src/components/ui/leg-press.tsx",
      "content": "\"use client\"\n\n/**\n * leg-press — a sled on inclined rails, where the rail angle *is* the\n * resistance.\n *\n * Only the component of the load that lies along the rails resists, so what the\n * sled actually weighs is `W · sin(θ)`: lay the frame at 30° and it hands back\n * half the plates, stand it at 45° and 0.71 of them, and at 0° the plates ride\n * for nothing. This is the one machine in the family where changing the\n * **frame** changes the weight — which is why a number off one leg press does\n * not compare with a number off another.\n *\n * `railAngle` is an input because it is a thing somebody bolted together. The\n * load is not: it comes back out of `solveSled`, and it is in the accessible\n * label rather than in a prop.\n *\n * The mechanism is solved in `src/lib/robocn/gym.ts` — pure, no React, tested on\n * its own. The drawing only reads the pose it produces.\n *\n * Illustrated: frictionless. No rail friction, no roller drag, no bearing loss,\n * and no dynamics — this is what the sled weighs standing still on the rails.\n *\n * Drawn once in the profile elevation and pushed through `robotCamera`, so all\n * four views are the same geometry rather than four drawings that drift apart.\n */\n\nimport * as React from \"react\"\n\nimport { arrowStep, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport { clamp, toRadians, type Vec2 } from \"@/lib/robocn/kinematics\"\nimport { solveSled, type SledGeometry } from \"@/lib/robocn/gym\"\nimport {\n  boxCorners,\n  elevationDraft,\n  fitTransform,\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\n/** What the machine does with nobody driving it. Always includes `static`. */\nexport type LegPressBehavior = \"press\" | \"partials\" | \"hold\" | \"static\"\n\nconst VIEW_WIDTH = 250\nconst VIEW_HEIGHT = 200\nconst NATIVE_VIEW: RobotView = \"profile\"\n\n\n/** Where the rails start, and how much rail there is past the travel. */\nconst RAIL_FOOT: Vec2 = { x: -40, y: 14 }\nconst TRAVEL = 96\nconst RAIL_OVERRUN = 26\nconst BASE_Y = 12\n/** Half the track between the two rails, out of the drawing plane. */\nconst RAIL_TRACK = 19\n/** How long the whole weight is drawn. The along-rail one is `sin(angle)` of it. */\nconst VECTOR = 42\n\n/**\n * The box the machine stands in, for one frame.\n *\n * It depends on the **rail angle** and on nothing else: the angle is a thing\n * somebody bolted together, so it may change the framing, while the travel is\n * the machine working and may not. Laying the frame down makes it long and low\n * and standing it up makes it tall, and either way the drawing fills its own\n * viewBox instead of floating in a box sized for the steepest frame it could be.\n */\nfunction envelopeFor(railAngle: number) {\n  const radians = toRadians(railAngle)\n  const top: Vec2 = {\n    x: RAIL_FOOT.x + Math.cos(radians) * (TRAVEL + RAIL_OVERRUN),\n    y: RAIL_FOOT.y + Math.sin(radians) * (TRAVEL + RAIL_OVERRUN),\n  }\n  // Drawing x runs along -z, so the box is written the way the camera reads it.\n  return boxCorners(\n    { x: -34, y: 0, z: -Math.max(62, top.x + 22) },\n    { x: 34, y: Math.max(76, top.y + 26), z: 100 },\n  )\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\n/**\n * Stroke at `clock`, 0 racked at the bottom to 1 fully extended. Every\n * behaviour is a pure function of the clock, exported so motion can be tested\n * by sampling it rather than by faking animation frames.\n */\nexport function legPressTravel(behavior: LegPressBehavior, clock: number): number {\n  if (behavior === \"static\" || !Number.isFinite(clock)) return 0.45\n  const t = ((clock % 1) + 1) % 1\n  switch (behavior) {\n    // Short reps at the top of the range, where the sled is easiest to hold.\n    case \"partials\":\n      return 0.62 + 0.3 * (1 - Math.cos(t * Math.PI * 8)) * 0.5\n    case \"hold\":\n      return clamp(0.72 + Math.sin(t * Math.PI * 12) * 0.02, 0, 1)\n    default:\n      return (1 - Math.cos(t * Math.PI * 2)) / 2\n  }\n}\n\nexport interface LegPressProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /**\n   * Controlled travel along the rails, 0 racked at the bottom to 1 fully\n   * extended. Supplying it stops the loop. Named for the rails rather than for\n   * the rep, because `stroke` is an SVG attribute this component passes through.\n   */\n  travel?: number\n  onTravelChange?: (stroke: number) => void\n  behavior?: LegPressBehavior\n  /** The rails' inclination from horizontal, in degrees. This is the resistance. */\n  railAngle?: number\n  /** Discs on the loading horns, per side. */\n  plates?: number\n  /** What one disc weighs, for the readout. */\n  plateWeight?: number\n  showGround?: boolean\n  /** Where the camera stands. Defaults to the view the machine was drawn in. */\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\nfunction LegPress({\n  travel,\n  onTravelChange,\n  behavior = \"press\",\n  railAngle = 38,\n  plates = 3,\n  plateWeight = 20,\n  showGround = true,\n  view = NATIVE_VIEW,\n  speed = 0.32,\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}: LegPressProps) {\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 controlled = travel !== undefined\n\n  // Controlled wins and pins the value; the clock keeps running underneath, so\n  // release reads as the sled coming back down rather than a jump.\n  const hold = controlled ? (Number.isFinite(travel) ? clamp(travel as number, 0, 1) : 0) : held\n  const goal = React.useCallback((clock: number) => legPressTravel(behavior, clock), [behavior])\n  const motion = useRobotScalar(goal, {\n    rate: 1.4,\n    hold,\n    speed,\n    paused,\n    phase,\n    animate: animate && !controlled && behavior !== \"static\",\n  })\n  const pushed = clamp(motion.value, 0, 1)\n\n  const discs = Math.max(0, Math.round(Number.isFinite(plates) ? plates : 3))\n  const perDisc = Math.abs(Number.isFinite(plateWeight) ? plateWeight : 20)\n  const sled: SledGeometry = React.useMemo(\n    () => ({\n      railAngle: Number.isFinite(railAngle) ? railAngle : 38,\n      travel: TRAVEL,\n      foot: RAIL_FOOT,\n      // Two horns, so what is on the sled is twice what is on one of them.\n      weight: discs * perDisc * 2,\n    }),\n    [railAngle, discs, perDisc],\n  )\n  const pose = solveSled(pushed, sled)\n\n  const camera = robotCamera(view)\n  const frame = fitTransform(envelopeFor(pose.railAngle), camera, VIEW_WIDTH, VIEW_HEIGHT)\n  const { point: to, path: line, solid, box, bar, disc } = elevationDraft(camera, \"profile\")\n\n  const apply = React.useCallback(\n    (next: number) => {\n      const bounded = Math.round(clamp(next, 0, 1) * 100) / 100\n      setHeld(bounded)\n      onTravelChange?.(bounded)\n    },\n    [onTravelChange],\n  )\n  const dragging = useRobotDrag(svgRef, {\n    // `onDrag` must stay in a `useCallback` or the listeners rebind every render.\n    enabled: interactive,\n    onDrag: React.useCallback((unit: Vec2) => apply(unit.x), [apply]),\n    onDragEnd: React.useCallback(() => setHeld(null), []),\n  })\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 rail line */\n\n  const radians = toRadians(pose.railAngle)\n  const along = (distance: number, offset = 0): Vec2 => ({\n    x: RAIL_FOOT.x + Math.cos(radians) * distance - Math.sin(radians) * offset,\n    y: RAIL_FOOT.y + Math.sin(radians) * distance + Math.cos(radians) * offset,\n  })\n  const railTop = along(TRAVEL + RAIL_OVERRUN)\n  const carriage = pose.carriage\n  // The footplate stands across the rails, so it turns with the frame.\n  const plateFoot = along(pose.along, 13)\n  const plateHead = along(pose.along, -21)\n  const horn = along(pose.along - 12, -4)\n  // The two load vectors hang from a point clear under the rails, where the\n  // sled is not sitting on top of them.\n  const hang = along(pose.along, -26)\n\n  const percent = Math.round(pushed * 100)\n  const loaded = Math.round(pose.load * 10) / 10\n  const share = Math.round(pose.fraction * 100)\n\n  return (\n    <svg\n      ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={\n        ariaLabel ??\n        `Leg press, rails at ${px(pose.railAngle)} degrees, ${sled.weight} on the sled and ${loaded} of it along the rails — ${share} percent, travel ${percent} percent, ${viewNames[view] ?? viewNames.profile}`\n      }\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? 1 : undefined}\n      aria-valuenow={interactive ? px(pushed) : undefined}\n      aria-valuetext={interactive ? `${percent} percent, sled load ${loaded}` : 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, 0.05, 0.2)\n        if (delta !== 0) apply(pushed + 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      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} transform={frame || undefined}>\n        {showGround && (\n          <>\n            <path\n              data-ground\n              d={solid([{ x: -98, y: 0 }, { x: 60, y: 0 }], 34)}\n              fill={palette.dark}\n              opacity={0.12}\n            />\n            <path\n              d={line([{ x: -98, y: 0 }, { x: 60, y: 0 }])}\n              fill=\"none\"\n              stroke={palette.dark}\n              strokeWidth={1}\n              opacity={0.5}\n            />\n          </>\n        )}\n\n        <g data-frame>\n          <path d={box(-96, 0, 56, BASE_Y, 30)} {...cast} />\n          {/* The seat the person is pushing away from. */}\n          <path d={box(-92, 26, -52, 35, 17)} {...shell} />\n          <path d={bar({ x: -88, y: 30 }, { x: -94, y: 62 }, 4.4, 16)} {...shell} />\n          <path d={bar({ x: -72, y: BASE_Y }, { x: -72, y: 26 }, 4, 6)} {...machined} />\n          {/* A post under the top of the rails, so the frame carries the sled. */}\n          <path\n            d={bar({ x: railTop.x - 4, y: BASE_Y }, { x: railTop.x - 4, y: railTop.y }, 4.5, 6)}\n            {...shell}\n          />\n          <path d={bar(RAIL_FOOT, { x: RAIL_FOOT.x, y: BASE_Y }, 5, 7)} {...shell} />\n        </g>\n\n        {/* The rails. Their angle is the only thing setting the resistance. */}\n        <g data-rails data-angle={px(pose.railAngle)}>\n          {[-1, 1].map((side) => (\n            <path\n              key={side}\n              d={bar(RAIL_FOOT, railTop, 2.6, 2.6, side * RAIL_TRACK)}\n              {...machined}\n            />\n          ))}\n        </g>\n\n        {/* The sled: carriage, footplate across the rails, and the discs. */}\n        <g data-sled data-load={px(pose.load)} data-along={px(pose.along)}>\n          <path d={bar(plateFoot, plateHead, 4.6, 16)} {...shell} />\n          <path d={bar(along(pose.along - 7, 0), along(pose.along + 7, 0), 7, 17)} {...cast} />\n          {[-1, 1].map((side) => (\n            <path\n              key={side}\n              d={bar(\n                along(pose.along - 4, 0),\n                along(pose.along - 26, 0),\n                2.4,\n                2.4,\n                side * (RAIL_TRACK + 7),\n              )}\n              {...machined}\n            />\n          ))}\n          {[-1, 1].map((side) =>\n            Array.from({ length: discs }, (_, index) => (\n              <g key={`${side}:${index}`} data-disc>\n                <path\n                  d={disc(\n                    along(pose.along - 11 - index * 6.5, -4),\n                    12 - index * 0.8,\n                    2.6,\n                    side * (RAIL_TRACK + 8),\n                  )}\n                  {...(index % 2 === 0 ? shell : machined)}\n                />\n                <path\n                  d={disc(\n                    along(pose.along - 11 - index * 6.5, -4),\n                    3.4,\n                    2.9,\n                    side * (RAIL_TRACK + 8),\n                  )}\n                  {...cast}\n                />\n              </g>\n            )),\n          )}\n          <path d={disc(horn, 2.6, 2.6, RAIL_TRACK + 8)} fill={palette.accent} stroke=\"none\" />\n        </g>\n\n        {/*\n          The load along the rails, drawn: the whole weight straight down from\n          the carriage, and the part of it the rails actually pass on.\n        */}\n        <g data-load-lines opacity={variant === \"wire\" ? 0.6 : 0.85}>\n          {/* The whole weight, straight down: the number on the plates. */}\n          <path\n            d={line([hang, { x: hang.x, y: hang.y - VECTOR }])}\n            fill=\"none\"\n            stroke={palette.foreground}\n            strokeWidth={1}\n            strokeDasharray=\"2.5 2.5\"\n            opacity={0.55}\n          />\n          {/* The part of it the rails pass on: `sin(angle)` of the same line. */}\n          <path\n            data-along-rail\n            d={line([hang, along(pose.along - VECTOR * pose.fraction, -26)])}\n            fill=\"none\"\n            stroke={palette.accent}\n            strokeWidth={2.4}\n            strokeLinecap=\"round\"\n          />\n          <path d={disc(hang, 1.8, 1.8)} fill={palette.accent} stroke=\"none\" />\n        </g>\n\n        {variant === \"blueprint\" && (\n          <text\n            x={px(to({ x: -10, y: 122 }).x)}\n            y={px(to({ x: -10, y: 122 }).y)}\n            textAnchor=\"middle\"\n            fontFamily=\"ui-monospace, monospace\"\n            fontSize={7}\n            fill={palette.foreground}\n          >\n            {`${px(pose.railAngle)}° · ${share}%`}\n          </text>\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\nexport { LegPress }\n",
      "type": "registry:ui",
      "target": "@ui/leg-press.tsx"
    }
  ],
  "categories": [
    "robotics",
    "gym"
  ],
  "type": "registry:ui"
}