{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "robot-scorpion",
  "title": "Scorpion",
  "description": "An eight-legged stalker in plan view with a metasoma solved in the sagittal plane, so the arch is a real height rather than a shorter drawing, plus hinged pedipalps and a tail strike on click.",
  "registryDependencies": [
    "https://robocn.dev/r/hexapod-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-scorpion.tsx",
      "content": "\"use client\"\n\n/**\n * robot-scorpion — eight legs and a tail that leaves the ground plane.\n *\n * The legs are the shared `solveHexapod` gait, the same one the spider and the\n * crab walk on. The metasoma is what is new: a `solveSpine` chain solved in the\n * animal's *sagittal* plane, so the solver's own `x` is how far back the tail\n * reaches and its `y` is how high — one curve that supplies both the plan\n * footprint and the true height. Arch it and the tail genuinely comes up and\n * over the back rather than being redrawn shorter. Click and it strikes.\n */\n\nimport * as React from \"react\"\n\nimport { usePointerTarget } from \"@/hooks/use-pointer-target\"\nimport { useRobotClock } from \"@/hooks/use-robot-motion\"\nimport { clamp, lerp, toRadians, type Vec2 } from \"@/lib/robocn/kinematics\"\nimport { solveHexapod, type HexapodGait, type HexapodLeg } from \"@/lib/robocn/hexapod\"\nimport { solveSpine } from \"@/lib/robocn/spine\"\nimport {\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\n/** Seconds a strike takes to whip over and come back. */\nconst STRIKE = 0.9\n/** How much of a raised joint's height shows as a screen offset in plan view. */\nconst RELIEF = 0.4\n\nexport type ScorpionBehavior = \"stalk\" | \"guard\" | \"strike\" | \"static\"\n\n/** Drawn from straight above; that is the camera it defaults to. */\nconst NATIVE_VIEW: RobotView = \"plan\"\n/** Where the tail leaves the body, behind the leg hub. */\nconst TAIL_ROOT = -26\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 RobotScorpionProps\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?: ScorpionBehavior\n  /** Footfall pattern. Omit and `behavior` picks one. */\n  gait?: HexapodGait\n  /** Controlled cycle fraction. Supplying it stops the internal clock. */\n  phase?: number\n  /** Gait cycles per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  /** Seconds of offset, so a nest breaks step. */\n  offset?: number\n  /** Walking legs, rounded to an even number and clamped 4–10. The pedipalps are extra. */\n  legs?: number\n  /** Normalized body clearance, foot travel, and swing height, each 0–1. */\n  height?: number\n  stride?: number\n  lift?: number\n  /** Travel direction in degrees: 0 walks toward the nose. */\n  heading?: number\n  /** Tail arch, 0 trailing flat to 1 curled over the back. Omit and the behavior sets it. */\n  arch?: number\n  /** Pedipalp opening, 0 shut and 1 spread. Omit and the behavior works them. */\n  claw?: number\n  /** Links in the tail, 3–24. */\n  segments?: number\n  /** It turns toward the pointer, and a click strikes with the tail. */\n  interactive?: boolean\n  onStrike?: () => void\n  size?: RobotSize | number\n  variant?: RobotVariant\n  showGround?: boolean\n  /** Mark the feet carrying weight. */\n  showContacts?: boolean\n  label?: string\n}\n\n/** Contour length of the metasoma. */\nconst TAIL = 108\n\nfunction RobotScorpion({\n  behavior = \"stalk\", gait, phase, view = NATIVE_VIEW, speed = 0.9, animate = true, paused = false, offset = 0,\n  legs = 8, height = 0.5, stride = 0.66, lift = 0.5, heading, arch, claw, segments = 9,\n  interactive = true, onStrike,\n  size = \"md\", variant = \"solid\", showGround = true, showContacts = false, label,\n  color, accent, metal, dark, glow, grid, palette: paletteOverride, className, style,\n  onPointerDown, ...props\n}: RobotScorpionProps) {\n  const controlled = phase !== undefined\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const clock = useRobotClock({\n    speed: 1,\n    animate: animate && !controlled && behavior !== \"static\",\n    paused,\n    phase: offset,\n  })\n\n  const [struck, setStruck] = React.useState<number | null>(null)\n  const since = struck === null ? Infinity : clock - struck\n  const whip = since >= 0 && since < STRIKE ? Math.sin((since / STRIKE) * Math.PI) : 0\n\n  const pointer = usePointerTarget(svgRef, {\n    enabled: interactive && heading === undefined && !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  const scripted = scorpionBehaviorPose(behavior, clock)\n  const course = finiteClamp(\n    heading ?? (pointer.target ? pointer.target.x * 55 : scripted.heading),\n    -180, 180, 0,\n  )\n  const cycle = controlled ? phase : clock * speed * scripted.rate\n  const beat = Number.isFinite(cycle) ? cycle : 0\n\n  const pose = solveHexapod({\n    legs,\n    gait: gait ?? scripted.gait,\n    phase: cycle,\n    height: clamp(height + scripted.bob, 0, 1),\n    stride,\n    lift,\n    heading: 0,\n    radius: 17,\n    fan: 142,\n    spread: 0.82,\n    femur: 21,\n    tibia: 25,\n  })\n\n  const curl = finiteClamp(clamp((arch ?? scripted.arch) + whip * 0.6, 0, 1), 0, 1, scripted.arch)\n  const gape = finiteClamp(claw ?? scripted.claw, 0, 1, 0.4)\n\n  // The tail is solved in the sagittal plane: the solver's x is how far back it\n  // reaches, its y is how high. One curve, both facts.\n  const tail = solveSpine({\n    segments,\n    length: TAIL,\n    phase: cycle,\n    amplitude: 0.07,\n    waves: 0.5,\n    taper: 0.8,\n    // Arc from a trailing tail to one curled right over the back.\n    turn: clamp(-(0.18 + 0.76 * curl), -1, 1),\n  })\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 ground = camera.plane()\n\n  /** A tail joint: how far behind the hub it reaches, and how high it is. */\n  const knot = (index: number) => {\n    const joint = tail.joints[index]\n    return {\n      // A small lateral wag, so the tail is not perfectly rigid in plan.\n      x: Math.sin(2 * Math.PI * beat) * 3 * joint.s,\n      y: TAIL_ROOT + joint.position.x,\n      height: pose.height + joint.position.y,\n      s: joint.s,\n    }\n  }\n  const tip = knot(tail.joints.length - 1)\n  /** A point in the plan drawing at `height` above the ground. */\n  const at = (p: { x: number; y: number }, h: number) => camera.project(p.x, h, -p.y)\n\n  function legDrawing(leg: HexapodLeg) {\n    const knee = { x: leg.knee.x, y: leg.knee.y - leg.kneeHeight * RELIEF }\n    const foot = { x: leg.foot.x, y: leg.foot.y - leg.clearance * RELIEF }\n    return (\n      <g key={leg.id} data-leg={leg.id} data-side={leg.side} opacity={leg.contact ? 1 : 0.82}>\n        <path d={capsulePath(leg.hip, knee, 3.1)} {...shell} />\n        <path d={capsulePath(knee, foot, 1.9)} {...machined} />\n        <circle cx={px(leg.hip.x)} cy={px(leg.hip.y)} r={3.2} {...cast} />\n        <circle cx={px(knee.x)} cy={px(knee.y)} r={2.5} {...cast} />\n        <circle cx={px(foot.x)} cy={px(foot.y)} r={1.5} fill={palette.dark} />\n        {showContacts && leg.contact && (\n          <circle data-contact cx={px(leg.foot.x)} cy={px(leg.foot.y)} r={3.2} fill=\"none\" stroke={palette.accent} strokeWidth={1} />\n        )}\n      </g>\n    )\n  }\n\n  /** A pedipalp: upper arm out, forearm forward, and a pincer that opens. */\n  function palp(side: 1 | -1) {\n    const name = side === 1 ? \"right\" : \"left\"\n    const shoulder = { x: side * 14, y: 20 }\n    const elbow = { x: side * 32, y: 36 }\n    const wrist = { x: side * 26, y: 56 }\n    const open = 6 + gape * 26\n    return (\n      <g data-claw={name}>\n        <path d={capsulePath(shoulder, elbow, 4)} {...shell} />\n        <path d={capsulePath(elbow, wrist, 3.2)} {...machined} />\n        <circle cx={px(elbow.x)} cy={px(elbow.y)} r={3.6} {...cast} />\n        <g transform={`translate(${px(wrist.x)} ${px(wrist.y)}) rotate(${px(side * 14)})`}>\n          <path d=\"M -6 -3 Q -7.5 9 0 13 Q 7.5 9 6 -3 Z\" {...shell} />\n          <g transform={`rotate(${px(-open)})`}>\n            <path d=\"M -2.6 7 L -2 23 L 2.4 21 L 2.8 8 Z\" {...cast} />\n          </g>\n          <g data-jaw transform={`rotate(${px(open)})`}>\n            <path d=\"M -2.6 7 L -2 23 L 2.4 21 L 2.8 8 Z\" {...machined} />\n          </g>\n          <circle cx={0} cy={2} r={2} fill={palette.accent} />\n        </g>\n      </g>\n    )\n  }\n\n  const state = whip > 0.05 ? \"striking\" : behavior === \"static\" ? \"still\" : behavior === \"guard\" ? \"on guard\" : behavior === \"strike\" ? \"striking\" : \"stalking\"\n\n  return (\n    <svg\n      ref={svgRef}\n      role=\"img\"\n      aria-label={`Robot scorpion, ${state}, tail ${Math.round(curl * 100)} percent arched, ${viewNames[view] ?? viewNames.plan}`}\n      viewBox=\"0 0 250 236\"\n      width={width}\n      height={px(width * 236 / 250)}\n      className={cn(\"max-w-full select-none\", interactive && \"cursor-pointer\", className)}\n      style={{ color: palette.foreground, ...style }}\n      onPointerDown={(event) => {\n        onPointerDown?.(event)\n        if (!interactive || event.defaultPrevented) return\n        setStruck(clock)\n        onStrike?.()\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 130 H 238 M 125 16 V 214\" strokeDasharray=\"2 3\" />\n          <path d={`M 125 130 l ${px(Math.sin(toRadians(course)) * 62)} ${px(-Math.cos(toRadians(course)) * 62)}`} strokeDasharray=\"4 3\" />\n        </g>\n      )}\n      {showGround && <ellipse cx={125} cy={130} rx={92} ry={66} fill={palette.dark} opacity={0.05} />}\n\n      {offAxis && <g data-solids transform=\"translate(125 130)\">\n        {pose.legs.map((leg) => (\n          <g key={leg.id} data-leg={leg.id} data-side={leg.side} opacity={leg.contact ? 1 : 0.82}>\n            <path d={capsulePath(at(leg.hip, pose.height), at(leg.knee, leg.kneeHeight), 3.1)} {...shell} />\n            <path d={capsulePath(at(leg.knee, leg.kneeHeight), at(leg.foot, leg.clearance), 1.9)} {...machined} />\n          </g>\n        ))}\n        {tail.joints.slice(0, -1).map((_, index) => {\n          const a = knot(index)\n          const b = knot(index + 1)\n          return (\n            <path\n              key={index}\n              data-segment={index}\n              d={capsulePath(at(a, a.height), at(b, b.height), px(4.4 - a.s * 1.6))}\n              {...(index % 2 === 0 ? shell : machined)}\n            />\n          )\n        })}\n        <path d={extrudedPath(roundedFootprint(17, 26, 9, 6), camera, pose.height + 7, pose.height - 6)} {...shell} />\n        {[-1, 1].map((side) => (\n          <path\n            key={side}\n            d={capsulePath(at({ x: side * 14, y: 20 }, pose.height), at({ x: side * 26, y: 56 }, pose.height - 2), 3.4)}\n            {...machined}\n          />\n        ))}\n      </g>}\n\n      <g\n        data-scorpion\n        data-view={view}\n        transform={`translate(125 130) ${ground} scale(1 -1) rotate(${px(-course)})`.replace(/\\s+/g, \" \")}\n      >\n        {pose.legs.map(legDrawing)}\n\n        {/* The metasoma. Height reads in plan as a shift toward the nose, so an\n            arched tail genuinely comes over the back instead of getting longer. */}\n        <g data-tail>\n          {tail.joints.slice(0, -1).map((_, index) => {\n            const a = knot(index)\n            const b = knot(index + 1)\n            const lift = (h: number) => (h - pose.height) * RELIEF\n            return (\n              <g key={index} data-segment={index}>\n                <path\n                  d={capsulePath(\n                    { x: a.x, y: a.y + lift(a.height) },\n                    { x: b.x, y: b.y + lift(b.height) },\n                    px(4.4 - a.s * 1.6),\n                  )}\n                  {...(index % 2 === 0 ? shell : machined)}\n                />\n                <circle cx={px(b.x)} cy={px(b.y + lift(b.height))} r={px(2.6 - b.s * 0.8)} {...cast} />\n              </g>\n            )\n          })}\n          <g data-sting transform={`translate(${px(tip.x)} ${px(tip.y + (tip.height - pose.height) * RELIEF)})`}>\n            <path d=\"M -4 -3 Q 0 9 4 -3 Q 0 -6 -4 -3 Z\" {...cast} />\n            <circle cx={0} cy={6} r={1.6} fill={palette.accent} />\n            <circle cx={0} cy={6} r={3.4} fill=\"none\" stroke={palette.glow} strokeWidth={0.8} opacity={px(0.2 + curl * 0.5)} />\n          </g>\n        </g>\n\n        <g data-body>\n          <path d=\"M -17 -26 Q -19 6 -12 24 L 12 24 Q 19 6 17 -26 Q 0 -31 -17 -26 Z\" {...shell} />\n          <g stroke={palette.dark} strokeWidth={0.9} opacity={0.4} fill=\"none\">\n            {[-18, -10, -2, 6, 14].map((y) => (\n              <path key={y} d={`M -15 ${y} H 15`} />\n            ))}\n          </g>\n          <rect x={-5} y={4} width={10} height={13} rx={3} {...cast} />\n          {/* The median eyes, on the ridge of the carapace. */}\n          {([-1, 1] as const).map((side) => (\n            <circle key={side} cx={px(side * 3.4)} cy={18} r={1.7} fill={palette.accent} />\n          ))}\n        </g>\n\n        <g data-chelicerae>\n          {([-1, 1] as const).map((side) => (\n            <path\n              key={side}\n              d={`M ${px(side * 4)} 24 q ${px(side * 3)} 5 ${px(side * 1)} 8`}\n              fill=\"none\"\n              stroke={palette.metal}\n              strokeWidth={2}\n              strokeLinecap=\"round\"\n            />\n          ))}\n        </g>\n\n        {palp(-1)}\n        {palp(1)}\n      </g>\n\n      {label && (\n        <text x={125} y={228} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\" fontSize={6} fill={palette.foreground}>\n          {label}\n        </text>\n      )}\n    </svg>\n  )\n}\n\n/** A control that has to survive a consumer handing it `NaN`. */\nconst finiteClamp = (value: number, min: number, max: number, fallback: number) =>\n  Number.isFinite(value) ? clamp(value, min, max) : fallback\n\n/** What it does with no timeline on it: stalk, stand guard, or whip the tail. */\nexport function scorpionBehaviorPose(behavior: ScorpionBehavior, clock: number) {\n  const time = Number.isFinite(clock) ? clock : 0\n  switch (behavior) {\n    // Standing its ground: tail right up, claws spread, feet planted.\n    case \"guard\":\n      return {\n        gait: \"stand\" as HexapodGait,\n        rate: 0,\n        heading: 24 * Math.sin(time * 0.3),\n        bob: 0.04 * Math.sin(time * 1.4),\n        arch: 0.92,\n        claw: 0.85 + 0.12 * Math.sin(time * 2),\n      }\n    // A strike is the tail loading and letting go, over and over.\n    case \"strike\": {\n      const load = Math.pow(Math.max(0, Math.sin(time * 1.6)), 4)\n      return {\n        gait: \"stand\" as HexapodGait,\n        rate: 0,\n        heading: 0,\n        bob: 0.03 * load,\n        arch: lerp(0.55, 1, load),\n        claw: 0.7,\n      }\n    }\n    case \"static\":\n      return { gait: \"stand\" as HexapodGait, rate: 0, heading: 0, bob: 0, arch: 0.55, claw: 0.4 }\n    // Low and forward, tail half up, casting for something to eat.\n    default:\n      return {\n        gait: \"tripod\" as HexapodGait,\n        rate: 1,\n        heading: 34 * Math.sin(time * 0.3),\n        bob: 0.02 * Math.sin(time * 6),\n        arch: 0.42 + 0.12 * Math.sin(time * 0.8),\n        claw: 0.35 + 0.2 * Math.sin(time * 1.1),\n      }\n  }\n}\n\nexport { RobotScorpion }\n",
      "type": "registry:ui",
      "target": "@ui/robot-scorpion.tsx"
    }
  ],
  "categories": [
    "robotics",
    "animals"
  ],
  "type": "registry:ui"
}