{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "robot-turtle",
  "title": "Turtle",
  "description": "A plodder in plan view: the gait solver at four legs on a slow wave, a procedurally plated carapace, and one number that pulls the head, tail and every foot in underneath it.",
  "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/use-pointer-target.json",
    "https://robocn.dev/r/use-robot-motion.json"
  ],
  "files": [
    {
      "path": "src/components/ui/robot-turtle.tsx",
      "content": "\"use client\"\n\n/**\n * robot-turtle — a plodder that can put itself away.\n *\n * `solveHexapod` always clamped to four legs and nothing had ever asked for\n * four; a slow wave gait at that count is a plod. What is its own is the\n * retraction: one number pulls the head, the tail and every foot in under the\n * carapace, and because the shell is drawn *after* the limbs, retracting is\n * geometry rather than a fade. The scutes are generated from the shell's own\n * outline, so a resized carapace re-plates itself. Click and it withdraws.\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 {\n  capsulePath,\n  circleFootprint,\n  extrudedPath,\n  px,\n  resolveRobotPalette,\n  resolveRobotSize,\n  robotCamera,\n  robotSurface,\n  type RobotPaletteProps,\n  type RobotSize,\n  type RobotVariant,\n  type RobotView,\n} from \"@/lib/robocn/style\"\nimport { cn } from \"@/lib/utils\"\n\nexport type TurtleBehavior = \"plod\" | \"bask\" | \"retract\" | \"static\"\n\n/** Drawn from straight above; that is the camera it defaults to. */\nconst NATIVE_VIEW: RobotView = \"plan\"\n/** How much of a raised joint's height shows as a screen offset in plan view. */\nconst RELIEF = 0.4\n/** Carapace half-width and half-length. */\nconst SHELL = { beam: 47, length: 57 } as const\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 RobotTurtleProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /** Where the camera stands. One plodder, four projections. */\n  view?: RobotView\n  /** What it does when `phase` is not supplied. */\n  behavior?: TurtleBehavior\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 line of them breaks step. */\n  offset?: 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  /** Retraction, 0 fully out to 1 everything under the shell. Omit and the behavior sets it. */\n  retract?: number\n  /** Head aim, −1..1. Omit and it follows the pointer. */\n  gaze?: number\n  /** The head tracks the pointer, and a click pulls everything in. */\n  interactive?: boolean\n  onRetractChange?: (withdrawn: boolean) => 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\nfunction RobotTurtle({\n  behavior = \"plod\", gait, phase, view = NATIVE_VIEW, speed = 0.45, animate = true, paused = false, offset = 0,\n  height = 0.35, stride = 0.55, lift = 0.4, heading, retract, gaze,\n  interactive = true, onRetractChange,\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}: RobotTurtleProps) {\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 [withdrawn, setWithdrawn] = React.useState(false)\n\n  const pointer = usePointerTarget(svgRef, {\n    enabled: interactive && gaze === 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 = turtleBehaviorPose(behavior, clock)\n  const inside = finiteClamp(retract ?? (withdrawn ? 1 : scripted.retract), 0, 1, scripted.retract)\n  const aim = finiteClamp(gaze ?? pointer.target?.x ?? scripted.gaze, -1, 1, 0)\n  const course = Number.isFinite(heading) ? (heading as number) : 0\n  const cycle = controlled ? phase : clock * speed * scripted.rate * (1 - inside)\n\n  const pose = solveHexapod({\n    legs: 4,\n    gait: gait ?? (inside > 0.5 ? \"stand\" : scripted.gait),\n    phase: cycle,\n    height: clamp(height + scripted.bob, 0, 1),\n    stride,\n    lift,\n    heading: 0,\n    radius: 30,\n    fan: 104,\n    // Withdrawing pulls the feet in under the shell rather than hiding them.\n    spread: lerp(0.92, 0.2, inside),\n    femur: 18,\n    tibia: 22,\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  const headY = lerp(88, 24, inside)\n  const tailY = lerp(-86, -42, inside)\n  const dome = 20 + 6 * (1 - inside)\n\n  /** A point in the plan drawing at `h` 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, 5.4)} {...machined} />\n        <path d={capsulePath(knee, foot, 4.4)} {...shell} />\n        <circle cx={px(knee.x)} cy={px(knee.y)} r={3.4} {...cast} />\n        {/* Claws on the front of each foot. */}\n        <g stroke={palette.dark} strokeWidth={1.4} strokeLinecap=\"round\" fill=\"none\">\n          {[-1, 0, 1].map((t) => (\n            <path key={t} d={`M ${px(foot.x + t * 3)} ${px(foot.y)} l ${px(t * 1.5)} 5`} />\n          ))}\n        </g>\n        {showContacts && leg.contact && (\n          <circle data-contact cx={px(leg.foot.x)} cy={px(leg.foot.y)} r={4.4} fill=\"none\" stroke={palette.accent} strokeWidth={1.1} />\n        )}\n      </g>\n    )\n  }\n\n  /** The scute plates, laid out from the carapace's own outline. */\n  const scutes = (() => {\n    const plates: { key: string; d: string; role: \"shell\" | \"metal\" }[] = []\n    // Five vertebral plates down the midline.\n    for (let index = 0; index < 5; index += 1) {\n      const y = 36 - index * 18\n      plates.push({\n        key: `v${index}`,\n        role: \"metal\",\n        d: hexagon(0, y, 14, 10),\n      })\n    }\n    // Four costal plates either side of them.\n    for (const side of [-1, 1] as const) {\n      for (let index = 0; index < 4; index += 1) {\n        const y = 27 - index * 18\n        plates.push({\n          key: `c${side}${index}`,\n          role: \"shell\",\n          d: hexagon(side * 26, y, 12, 9),\n        })\n      }\n    }\n    // A marginal ring following the shell edge.\n    for (let index = 0; index < 16; index += 1) {\n      const angle = (index / 16) * Math.PI * 2\n      plates.push({\n        key: `m${index}`,\n        role: \"metal\",\n        d: hexagon(Math.cos(angle) * (SHELL.beam - 7), Math.sin(angle) * (SHELL.length - 7), 7, 6),\n      })\n    }\n    return plates\n  })()\n\n  const state = behavior === \"static\" ? \"still\" : inside > 0.5 ? \"withdrawn\" : behavior === \"bask\" ? \"basking\" : \"plodding\"\n\n  return (\n    <svg\n      ref={svgRef}\n      role=\"img\"\n      aria-label={`Robot turtle, ${state}, ${viewNames[view] ?? viewNames.plan}`}\n      viewBox=\"0 0 240 230\"\n      width={width}\n      height={px(width * 230 / 240)}\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        const next = !withdrawn\n        setWithdrawn(next)\n        onRetractChange?.(next)\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 114 H 228 M 120 14 V 214\" strokeDasharray=\"2 3\" />\n          <ellipse cx={120} cy={114} rx={SHELL.beam} ry={SHELL.length} strokeDasharray=\"3 4\" />\n        </g>\n      )}\n      {showGround && <ellipse cx={120} cy={114} rx={96} ry={80} fill={palette.dark} opacity={0.05} />}\n\n      {offAxis && <g data-solids transform=\"translate(120 114)\">\n        {pose.legs.map((leg) => (\n          <g key={leg.id} data-leg={leg.id} data-side={leg.side}>\n            <path d={capsulePath(at(leg.hip, pose.height), at(leg.knee, leg.kneeHeight), 5)} {...machined} />\n            <path d={capsulePath(at(leg.knee, leg.kneeHeight), at(leg.foot, leg.clearance), 4)} {...shell} />\n          </g>\n        ))}\n        <path d={capsulePath(at({ x: 0, y: 30 }, pose.height + 8), at({ x: 0, y: headY }, pose.height + 10), 9)} {...machined} />\n        <path d={capsulePath(at({ x: 0, y: -40 }, pose.height + 4), at({ x: 0, y: tailY }, pose.height + 2), 4)} {...cast} />\n        {/* The carapace is a dome, which only a second camera can show. */}\n        <path\n          d={extrudedPath(circleFootprint(0, 0, SHELL.beam * 0.82, 16), camera, pose.height + dome, pose.height - 3)}\n          {...shell}\n        />\n      </g>}\n\n      <g\n        data-turtle\n        data-view={view}\n        transform={`translate(120 114) ${ground} scale(1 -1) rotate(${px(-course)})`.replace(/\\s+/g, \" \")}\n      >\n        {pose.legs.map(legDrawing)}\n\n        <g data-tail>\n          <path d={capsulePath({ x: 0, y: -46 }, { x: px(aim * -3), y: tailY }, px(5 - inside * 1.5))} {...cast} />\n        </g>\n\n        <g data-head transform={`translate(0 ${px(headY - 16)}) rotate(${px(-aim * 22 * (1 - inside))})`}>\n          <path d={capsulePath({ x: 0, y: -18 }, { x: 0, y: 2 }, 7.5)} {...machined} />\n          <path d=\"M -9 2 Q -10 16 0 19 Q 10 16 9 2 Q 0 -2 -9 2 Z\" {...machined} />\n          <path d=\"M -6 15 Q 0 18 6 15\" fill=\"none\" stroke={palette.dark} strokeWidth={1} />\n          {([-1, 1] as const).map((side) => (\n            <g key={side} data-eye={side === 1 ? \"right\" : \"left\"}>\n              <circle cx={px(side * 5)} cy={9} r={2.8} {...cast} />\n              <circle cx={px(side * 5 + aim * 0.9)} cy={9.4} r={1.4} fill={palette.accent} />\n            </g>\n          ))}\n          <g stroke={palette.dark} strokeWidth={0.7} opacity={0.4} fill=\"none\">\n            <path d=\"M -7 -4 H 7 M -6 -10 H 6\" />\n          </g>\n        </g>\n\n        {/* The carapace, drawn last: whatever is retracted goes under it. */}\n        <g data-carapace>\n          <ellipse cx={0} cy={0} rx={SHELL.beam} ry={SHELL.length} {...cast} />\n          <ellipse cx={0} cy={0} rx={px(SHELL.beam - 5)} ry={px(SHELL.length - 5)} {...shell} />\n          <g opacity={0.95}>\n            {scutes.map((plate, index) => (\n              <path\n                key={plate.key}\n                data-scute={index}\n                d={plate.d}\n                {...(plate.role === \"metal\" ? machined : shell)}\n                fillOpacity={variant === \"solid\" ? 0.55 : undefined}\n              />\n            ))}\n          </g>\n          <circle cx={0} cy={0} r={4} fill={palette.accent} opacity={0.85} />\n          <circle cx={0} cy={0} r={9} fill=\"none\" stroke={palette.glow} strokeWidth={0.9} opacity={0.4} />\n        </g>\n      </g>\n\n      {label && (\n        <text x={120} y={222} 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/** A flat-topped hexagonal plate. */\nfunction hexagon(cx: number, cy: number, rx: number, ry: number) {\n  return Array.from({ length: 6 }, (_, index) => {\n    const angle = toRadians(index * 60 + 30)\n    return `${index ? \"L\" : \"M\"} ${px(cx + Math.cos(angle) * rx)} ${px(cy + Math.sin(angle) * ry)}`\n  }).join(\" \") + \" Z\"\n}\n\n/** What it does with no timeline on it: plod, sit in the sun, or shut up shop. */\nexport function turtleBehaviorPose(behavior: TurtleBehavior, clock: number) {\n  const time = Number.isFinite(clock) ? clock : 0\n  switch (behavior) {\n    // Feet planted, neck right out, turning slowly to follow the light.\n    case \"bask\":\n      return { gait: \"stand\" as HexapodGait, rate: 0, retract: 0, bob: 0.02 * Math.sin(time * 0.5), gaze: 0.7 * Math.sin(time * 0.22) }\n    case \"retract\":\n      return { gait: \"stand\" as HexapodGait, rate: 0, retract: 1, bob: 0, gaze: 0 }\n    case \"static\":\n      return { gait: \"stand\" as HexapodGait, rate: 0, retract: 0.15, bob: 0, gaze: 0 }\n    // One leg at a time, which is what makes it a plod rather than a walk.\n    default:\n      return {\n        gait: \"wave\" as HexapodGait,\n        rate: 1,\n        retract: 0.05,\n        bob: 0.02 * Math.sin(time * 2.4),\n        gaze: 0.35 * Math.sin(time * 0.4),\n      }\n  }\n}\n\nexport { RobotTurtle }\n",
      "type": "registry:ui",
      "target": "@ui/robot-turtle.tsx"
    }
  ],
  "categories": [
    "robotics",
    "animals"
  ],
  "type": "registry:ui"
}