{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rail-bogie",
  "title": "Bogie",
  "description": "A powered two-axle bogie whose wheelsets hunt because they are coned, at exactly Klingel wavelength, with a frame that is not animated but placed by the two wheelsets it joins.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/rail-geometry.json",
    "https://robocn.dev/r/use-robot-motion.json"
  ],
  "files": [
    {
      "path": "src/components/ui/rail-bogie.tsx",
      "content": "\"use client\"\n\n/**\n * rail-bogie — a powered two-axle bogie, and the only self-excited motion in\n * the set.\n *\n * Nothing commands a wheelset to wander. It wanders because it is *coned*:\n * displace it and the two wheels roll on different radii, which yaws it;\n * yaw it and it runs sideways. That loop has no damping in it, so the wheelset\n * weaves down the track at exactly Klingel's wavelength — a length that\n * depends on the tread, the wheel and the gauge and on nothing else, least of\n * all speed. Take the cone away and the motion stops dead and the wavelength\n * goes to infinity, which is the one control on this machine worth touching.\n *\n * The frame is not animated either: it connects two wheelsets that are each at\n * their own point of the same wave, so its lateral position is their mean and\n * its yaw is the line between them. What the primary suspension has to take is\n * then the difference, and the axleboxes show it.\n *\n * No dynamics: no speed, no creep forces, no damping, no critical speed. The\n * amplitude is an input, because the kinematic solution does not set one —\n * what does set it is the flange, and that is a clamp.\n */\n\nimport * as React from \"react\"\n\nimport { arrowStep, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport { clamp, toDegrees, toRadians, type Vec2, type Vec3 } from \"@/lib/robocn/kinematics\"\nimport {\n  boxCorners,\n  circleFootprint,\n  fitTransform,\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 { huntingPose, klingelWavelength, radialYaw } from \"@/lib/robocn/rail\"\nimport { cn } from \"@/lib/utils\"\n\nconst VIEW_WIDTH = 190\nconst VIEW_HEIGHT = 190\nconst NATIVE_VIEW: RobotView = \"plan\"\n\n/** The bogie in world units: x starboard, y up from the rail head, z aft. */\nconst HALF_GAUGE = 33\nconst WHEEL_RADIUS = 19\nconst WHEEL_HALF_WIDTH = 6\nconst WHEELBASE = 96\nconst FRAME_HALF_WIDTH = 48\nconst FRAME_HALF_LENGTH = 66\nconst FRAME_TOP = 30\nconst FRAME_BOTTOM = 21\nconst RAIL_RUN = 92\nconst RAIL_HEAD = 3\nconst FLANGE_CLEARANCE = 8\n/** Axlebox centre: outboard of the wheel, under the frame's side beam. */\nconst AXLEBOX = HALF_GAUGE + WHEEL_HALF_WIDTH + 9\n/** Wheel diameters of run per second while the travel eases back into a behaviour. */\nconst RUN_RATE = 80\nconst MAX_RUN = 40\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 far the bogie runs into a curve under the `curve` behaviour. A stated radius. */\nconst CURVE_RADIUS = 520\n\nexport type RailBogieBehavior = \"hunt\" | \"curve\" | \"brake\" | \"static\"\n\nexport interface RailBogieProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /** Distance run, in wheel diameters. Supplying it stops the loop. */\n  travel?: number\n  onTravelChange?: (travel: number) => void\n  /** What the bogie does when `travel` is not supplied. */\n  behavior?: RailBogieBehavior\n  /**\n   * Tread conicity — the tan of the cone angle, 0 to 0.4. This is the whole\n   * mechanism: zero is a cylindrical tread and the hunting stops.\n   */\n  conicity?: number\n  /** How far the wheelset wanders before a flange finds a rail, in world units. */\n  amplitude?: number\n  /** Brake shoes on the treads, 0 off to 1 hard on. Omit and the behaviour works them. */\n  brake?: number\n  view?: RobotView\n  /** The rails and sleepers under it. */\n  showTrack?: boolean\n  /** Light the traction motor and call out a flanging wheelset. */\n  active?: boolean\n  interactive?: boolean\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  phase?: number\n  label?: string\n  size?: RobotSize | number\n  variant?: RobotVariant\n}\n\nfunction RailBogie({\n  travel,\n  onTravelChange,\n  behavior = \"hunt\",\n  conicity = 0.1,\n  amplitude = 7,\n  brake,\n  view = NATIVE_VIEW,\n  showTrack = true,\n  active,\n  interactive = false,\n  speed = 0.24,\n  animate = true,\n  paused = false,\n  phase = 0,\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}: RailBogieProps) {\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  const hold = controlled\n    ? Number.isFinite(travel) ? clamp(travel as number, 0, MAX_RUN) : 0\n    : held\n  const goal = React.useCallback(\n    (clock: number) => bogieRun(behavior, clock),\n    [behavior],\n  )\n  const motion = useRobotScalar(goal, {\n    rate: RUN_RATE,\n    hold,\n    speed,\n    paused,\n    phase,\n    animate: animate && !controlled && behavior !== \"static\",\n  })\n  const run = clamp(motion.value, 0, MAX_RUN)\n  const clock = Number.isFinite(motion.clock) ? motion.clock : 0\n  const shoes = brake !== undefined\n    ? Number.isFinite(brake) ? clamp(brake, 0, 1) : 0\n    : bogieBrake(behavior, clock)\n\n  const apply = React.useCallback(\n    (next: number) => {\n      const bounded = Math.round(clamp(next, 0, MAX_RUN) * 100) / 100\n      setHeld(bounded)\n      onTravelChange?.(bounded)\n    },\n    [onTravelChange],\n  )\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    onDrag: React.useCallback((unit: Vec2) => apply(unit.x * MAX_RUN), [apply]),\n    onDragEnd: React.useCallback(() => setHeld(null), []),\n  })\n\n  /* ---- what the cone does ---- */\n\n  const wheelset = {\n    wheelRadius: WHEEL_RADIUS,\n    halfGauge: HALF_GAUGE,\n    conicity: clamp(Number.isFinite(conicity) ? conicity : 0.1, 0, 0.4),\n    flangeClearance: FLANGE_CLEARANCE,\n  }\n  const wander = clamp(Number.isFinite(amplitude) ? amplitude : 7, 0, FLANGE_CLEARANCE * 2)\n  const wavelength = klingelWavelength(wheelset)\n  const distance = run * WHEEL_RADIUS * 2\n  // Braking takes the run out of it, so the weave dies away with the speed.\n  const working = wander * (1 - shoes * 0.85)\n  // The two wheelsets are a wheelbase apart on one wave, so they are never at\n  // the same point of it — which is what yaws the frame as well.\n  const lead = huntingPose(distance + WHEELBASE / 2, working, wheelset)\n  const trail = huntingPose(distance - WHEELBASE / 2, working, wheelset)\n  // On a curve both wheelsets stay on the track and stand *radially* — each\n  // square to the radius at its own position — so they splay against each\n  // other by twice the half-wheelbase angle.\n  const radial = behavior === \"curve\" && !controlled ? 1 : 0\n  const splay = radialYaw(WHEELBASE / 2, CURVE_RADIUS)\n  const leadYaw = lead.yaw + radial * splay\n  const trailYaw = trail.yaw - radial * splay\n  const leadLateral = lead.lateral\n  const trailLateral = trail.lateral\n\n  // The frame is not animated: it joins two wheelsets, so it sits on their\n  // mean and points along the line between them. On a curve it is the chord\n  // between two points that are on the track, so it also stands `R(1 − cos α)`\n  // *inside* it — the bogie's own share of the centre throw.\n  const insideThrow = CURVE_RADIUS * (1 - Math.cos(WHEELBASE / 2 / CURVE_RADIUS))\n  const frameLateral = (leadLateral + trailLateral) / 2 + radial * insideThrow\n  const frameYaw = toDegrees(Math.atan2(leadLateral - trailLateral, WHEELBASE))\n\n  const camera = robotCamera(view)\n  const fit = fitTransform(\n    boxCorners(\n      { x: -(FRAME_HALF_WIDTH + 9), y: -2, z: -RAIL_RUN },\n      { x: FRAME_HALF_WIDTH + 9, y: FRAME_TOP + 14, z: RAIL_RUN },\n    ),\n    camera,\n    VIEW_WIDTH,\n    VIEW_HEIGHT,\n  )\n\n  /** A part in a yawed, laterally displaced frame: a wheelset, or the bogie. */\n  const framed = (centre: number, lateral: number, yaw: number) => {\n    const turn = toRadians(yaw)\n    const cos = Math.cos(turn)\n    const sin = Math.sin(turn)\n    return (point: Vec3): Vec3 => ({\n      x: lateral + point.x * cos + point.z * sin,\n      y: point.y,\n      z: centre - point.x * sin + point.z * cos,\n    })\n  }\n\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n  const live = active ?? behavior !== \"static\"\n\n  /** A box in a placed frame. */\n  const box = (\n    place: (point: Vec3) => Vec3,\n    x0: number,\n    z0: number,\n    x1: number,\n    z1: number,\n    bottom: number,\n    top: number,\n  ) =>\n    slabPath(\n      [\n        { x: x0, z: z0 },\n        { x: x1, z: z0 },\n        { x: x1, z: z1 },\n        { x: x0, z: z1 },\n      ].flatMap((corner) => [\n        place({ x: corner.x, y: bottom, z: corner.z }),\n        place({ x: corner.x, y: top, z: corner.z }),\n      ]),\n      camera,\n    )\n\n  /** A wheel: a disc standing in the wheel's own plane. */\n  const wheelPath = (place: (point: Vec3) => Vec3, side: number) =>\n    slabPath(\n      Array.from({ length: 18 }, (_, index) => {\n        const angle = (index / 18) * Math.PI * 2\n        return {\n          y: WHEEL_RADIUS + Math.sin(angle) * WHEEL_RADIUS,\n          z: Math.cos(angle) * WHEEL_RADIUS,\n        }\n      }).flatMap((point) => [\n        place({ x: side * HALF_GAUGE + WHEEL_HALF_WIDTH, y: point.y, z: point.z }),\n        place({ x: side * HALF_GAUGE - WHEEL_HALF_WIDTH, y: point.y, z: point.z }),\n      ]),\n      camera,\n    )\n\n  /** A coil spring, drawn as the stack of rings it is. */\n  const spring = (place: (point: Vec3) => Vec3, x: number, z: number, radius: number) =>\n    [0, 1, 2].map((ring) =>\n      slabPath(\n        circleFootprint(x, z, radius, 12).map((point) =>\n          place({ x: point.x, y: WHEEL_RADIUS + 1 + ring * 2.6, z: point.y }),\n        ),\n        camera,\n      ),\n    )\n\n  const wheelsets = [\n    {\n      name: \"lead\",\n      centre: -WHEELBASE / 2,\n      lateral: leadLateral,\n      yaw: leadYaw,\n      pose: lead,\n      driven: true,\n    },\n    {\n      name: \"trail\",\n      centre: WHEELBASE / 2,\n      lateral: trailLateral,\n      yaw: trailYaw,\n      pose: trail,\n      driven: false,\n    },\n  ].map((entry) => ({ ...entry, place: framed(entry.centre, entry.lateral, entry.yaw) }))\n\n  const frame = framed(0, frameLateral, frameYaw)\n  const frameDepth = camera.depth(frameLateral, FRAME_BOTTOM, 0)\n  /** Is this wheel in front of the frame from where the camera stands? */\n  const near = (entry: (typeof wheelsets)[number], side: number) =>\n    camera.depth(entry.lateral + side * HALF_GAUGE, WHEEL_RADIUS, entry.centre) > frameDepth\n  const rails = [-1, 1].map((side) => ({\n    side,\n    name: side < 0 ? \"port\" : \"starboard\",\n    depth: camera.depth(side * HALF_GAUGE, RAIL_HEAD, 0),\n    path: box(\n      (point: Vec3) => point,\n      side * HALF_GAUGE - 3,\n      -RAIL_RUN,\n      side * HALF_GAUGE + 3,\n      RAIL_RUN,\n      0,\n      RAIL_HEAD,\n    ),\n  }))\n  /** A tread, lit when its flange has found a rail. */\n  const tread = (flanging: boolean) => ({\n    ...cast,\n    fill: flanging && live ? palette.accent : cast.fill,\n  })\n  const readout = Math.round(run * 10) / 10\n\n  return (\n    <svg\n      ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={\n        ariaLabel ??\n        `Rail bogie, run ${readout} wheel diameters, conicity ${wheelset.conicity.toFixed(2)}, ${\n          Number.isFinite(wavelength)\n            ? `hunting wavelength ${Math.round(wavelength)} units`\n            : \"cylindrical treads and no hunting\"\n        }, ${viewNames[view] ?? viewNames.plan}`\n      }\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? MAX_RUN : undefined}\n      aria-valuenow={interactive ? px(run) : undefined}\n      aria-valuetext={interactive ? `${readout} wheel diameters run` : 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, event.shiftKey ? 2 : 0.5, 5)\n        if (delta !== 0) apply(run + delta)\n        else if (event.key === \"Home\") apply(0)\n        else if (event.key === \"End\") apply(MAX_RUN)\n        else if (event.key === \"Escape\") setHeld(null)\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      <g\n        data-view={view}\n        data-travel={px(run)}\n        data-wavelength={Number.isFinite(wavelength) ? px(wavelength) : \"infinite\"}\n        data-conicity={px(wheelset.conicity)}\n        transform={fit || undefined}\n      >\n        {variant === \"blueprint\" && (\n          <path\n            data-centreline\n            d={(() => {\n              const a = camera.project(0, 0, -RAIL_RUN)\n              const b = camera.project(0, 0, RAIL_RUN)\n              return `M ${px(a.x)} ${px(a.y)} L ${px(b.x)} ${px(b.y)}`\n            })()}\n            fill=\"none\"\n            stroke={palette.grid}\n            strokeWidth={0.5}\n            strokeDasharray=\"3 4\"\n            opacity={0.7}\n          />\n        )}\n        {showTrack && (\n          <g data-track>\n            {rails\n              .filter((rail) => rail.depth <= frameDepth)\n              .map((rail) => (\n                <path key={rail.side} data-rail={rail.name} d={rail.path} {...machined} />\n              ))}\n            {[-72, -24, 24, 72].map((at) => (\n              <path\n                key={at}\n                data-sleeper={at}\n                d={box((point) => point, -HALF_GAUGE - 13, at - 6, HALF_GAUGE + 13, at + 6, 0, 1.6)}\n                fill={palette.dark}\n                fillOpacity={variant === \"solid\" ? 0.28 : 0.12}\n                stroke=\"none\"\n              />\n            ))}\n          </g>\n        )}\n\n        {wheelsets.map((entry) => (\n          <g\n            key={entry.name}\n            data-wheelset={entry.name}\n            data-lateral={px(entry.lateral)}\n            data-yaw={px(entry.yaw)}\n            data-flanging={entry.pose.flanging ? \"true\" : \"false\"}\n          >\n            {/* The axle: what makes the two wheels one body, and the reason a\n                rolling-radius difference has to come out as yaw. */}\n            <path\n              data-axle={entry.name}\n              d={box(entry.place, -AXLEBOX, -3.4, AXLEBOX, 3.4, WHEEL_RADIUS - 3.4, WHEEL_RADIUS + 3.4)}\n              {...machined}\n            />\n            {[-1, 1].map((side) => (\n              <g key={side}>\n                {!near(entry, side) && (\n                  <path\n                    data-wheel={`${entry.name}-${side < 0 ? \"port\" : \"starboard\"}`}\n                    d={wheelPath(entry.place, side)}\n                    {...tread(entry.pose.flanging)}\n                  />\n                )}\n                <path\n                  data-flange={`${entry.name}-${side < 0 ? \"port\" : \"starboard\"}`}\n                  d={slabPath(\n                    Array.from({ length: 18 }, (_, index) => {\n                      const angle = (index / 18) * Math.PI * 2\n                      return {\n                        y: WHEEL_RADIUS + Math.sin(angle) * (WHEEL_RADIUS + 3),\n                        z: Math.cos(angle) * (WHEEL_RADIUS + 3),\n                      }\n                    }).flatMap((point) => [\n                      entry.place({ x: side * (HALF_GAUGE - WHEEL_HALF_WIDTH), y: point.y, z: point.z }),\n                      entry.place({ x: side * (HALF_GAUGE - WHEEL_HALF_WIDTH - 2.5), y: point.y, z: point.z }),\n                    ]),\n                    camera,\n                  )}\n                  fill={entry.pose.flanging && live ? palette.accent : palette.metal}\n                  fillOpacity={variant === \"solid\" ? 1 : 0.25}\n                  stroke={palette.dark}\n                  strokeWidth={0.4}\n                />\n                {/* Axlebox: the wheelset's only connection to the frame, so\n                    this is where the primary suspension's travel shows. */}\n                <path\n                  data-axlebox={`${entry.name}-${side < 0 ? \"port\" : \"starboard\"}`}\n                  d={box(\n                    entry.place,\n                    side * AXLEBOX - 7,\n                    -8,\n                    side * AXLEBOX + 7,\n                    8,\n                    WHEEL_RADIUS - 6,\n                    WHEEL_RADIUS + 4,\n                  )}\n                  {...cast}\n                />\n                {spring(entry.place, side * AXLEBOX, 0, 5.5).map((ring, index) => (\n                  <path\n                    key={index}\n                    data-primary={`${entry.name}-${side < 0 ? \"port\" : \"starboard\"}`}\n                    d={ring}\n                    fill=\"none\"\n                    stroke={palette.metal}\n                    strokeWidth={1.3}\n                  />\n                ))}\n                {/* Brake shoes, which come onto the tread rather than appear. */}\n                <path\n                  data-brake={`${entry.name}-${side < 0 ? \"port\" : \"starboard\"}`}\n                  data-application={px(shoes)}\n                  d={box(\n                    entry.place,\n                    side * HALF_GAUGE - WHEEL_HALF_WIDTH,\n                    13.5 - shoes * 2.5,\n                    side * HALF_GAUGE + WHEEL_HALF_WIDTH,\n                    20.5 - shoes * 2.5,\n                    1.5,\n                    9.5,\n                  )}\n                  fill={shoes > 0.05 ? palette.accent : palette.metal}\n                  fillOpacity={variant === \"solid\" ? (shoes > 0.05 ? 1 : 0.8) : 0.3}\n                  stroke={palette.dark}\n                  strokeWidth={0.5}\n                />\n              </g>\n            ))}\n            {entry.driven && (\n              <>\n                <path\n                  data-motor\n                  d={box(entry.place, -17, 9, 17, 31, WHEEL_RADIUS - 9, WHEEL_RADIUS + 6)}\n                  {...machined}\n                />\n                <path\n                  data-motor-terminal\n                  d={box(entry.place, -6, 12, 6, 17, WHEEL_RADIUS + 6, WHEEL_RADIUS + 9)}\n                  fill={live ? palette.accent : palette.metal}\n                  fillOpacity={variant === \"solid\" ? 1 : 0.3}\n                  stroke={palette.dark}\n                  strokeWidth={0.4}\n                />\n              </>\n            )}\n          </g>\n        ))}\n\n        <g data-frame data-yaw={px(frameYaw)} data-lateral={px(frameLateral)}>\n          {/* Two side beams and a transom: an H, which is why the wheelsets\n              can yaw against it at all. */}\n          {[-1, 1].map((side) => (\n            <path\n              key={side}\n              data-sideframe={side < 0 ? \"port\" : \"starboard\"}\n              d={box(\n                frame,\n                side * FRAME_HALF_WIDTH - 8,\n                -FRAME_HALF_LENGTH,\n                side * FRAME_HALF_WIDTH + 8,\n                FRAME_HALF_LENGTH,\n                FRAME_BOTTOM,\n                FRAME_TOP,\n              )}\n              {...shell}\n            />\n          ))}\n          <path\n            data-transom\n            d={box(frame, -FRAME_HALF_WIDTH, -13, FRAME_HALF_WIDTH, 13, FRAME_BOTTOM, FRAME_TOP)}\n            {...shell}\n          />\n          {/* Secondary suspension: what the body above actually rides on. */}\n          {[-1, 1].map((side) => (\n            <path\n              key={side}\n              data-secondary={side < 0 ? \"port\" : \"starboard\"}\n              d={slabPath(\n                circleFootprint(side * 26, 0, 12, 14).flatMap((point) => [\n                  frame({ x: point.x, y: FRAME_TOP, z: point.y }),\n                  frame({ x: point.x, y: FRAME_TOP + 9, z: point.y }),\n                ]),\n                camera,\n              )}\n              {...machined}\n            />\n          ))}\n          <path\n            data-centre-pivot\n            d={slabPath(\n              circleFootprint(0, 0, 9, 14).flatMap((point) => [\n                frame({ x: point.x, y: FRAME_TOP, z: point.y }),\n                frame({ x: point.x, y: FRAME_TOP + 12, z: point.y }),\n              ]),\n              camera,\n            )}\n            {...cast}\n          />\n        </g>\n\n        {showTrack &&\n          rails\n            .filter((rail) => rail.depth > frameDepth)\n            .map((rail) => (\n              <path key={rail.side} data-rail={rail.name} d={rail.path} {...machined} />\n            ))}\n\n        {/* The wheels that finished up in front of the frame paint over it. */}\n        {wheelsets.flatMap((entry) =>\n          [-1, 1].filter((side) => near(entry, side)).map((side) => (\n            <path\n              key={`${entry.name}-${side}`}\n              data-wheel={`${entry.name}-${side < 0 ? \"port\" : \"starboard\"}`}\n              d={wheelPath(entry.place, side)}\n              {...tread(entry.pose.flanging)}\n            />\n          )),\n        )}\n      </g>\n\n      {label && (\n        <text\n          x={VIEW_WIDTH / 2}\n          y={VIEW_HEIGHT - 5}\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/** How far the bogie has run at `clock`, in wheel diameters. */\nexport function bogieRun(behavior: RailBogieBehavior, clock: number): number {\n  if (behavior === \"static\" || !Number.isFinite(clock)) return 0\n  const cycle = ((clock % 1) + 1) % 1\n  if (behavior === \"brake\") {\n    // Running, then stopping: the run flattens out as the shoes go on.\n    return MAX_RUN * (1 - (1 - Math.min(1, cycle / 0.75)) ** 2)\n  }\n  // Out and back, so the cycle closes without the run ever jumping — the\n  // hunting pose is symmetric in distance, so a bench can be run either way.\n  return (cycle < 0.5 ? cycle * 2 : (1 - cycle) * 2) * MAX_RUN\n}\n\n/** How hard the shoes are on the treads at `clock`, 0 off to 1 hard on. */\nexport function bogieBrake(behavior: RailBogieBehavior, clock: number): number {\n  if (behavior !== \"brake\" || !Number.isFinite(clock)) return 0\n  const cycle = ((clock % 1) + 1) % 1\n  if (cycle < 0.15) return 0\n  if (cycle < 0.55) return (cycle - 0.15) / 0.4\n  if (cycle < 0.85) return 1\n  return 1 - (cycle - 0.85) / 0.15\n}\n\nexport { RailBogie }\n",
      "type": "registry:ui",
      "target": "@ui/rail-bogie.tsx"
    }
  ],
  "categories": [
    "robotics",
    "rail"
  ],
  "type": "registry:ui"
}