{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "robot-fish",
  "title": "Fish",
  "description": "A swimming fish in profile: a tail-weighted body wave, solved hull outline, dorsal and pectoral fins, pointer steering, and a dart on click. Drawn from any of four camera angles.",
  "registryDependencies": [
    "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-fish.tsx",
      "content": "\"use client\"\n\n/**\n * robot-fish — a swimming machine in profile.\n *\n * The body is the solver's output, not artwork: `solveSpine` bends a serpenoid\n * wave with the swing piled at the tail, and the hull is that spine offset to\n * either side. Left alone it cruises, darts or holds station on fins; hand it\n * `phase` and your timeline drives the beat instead. It turns toward the\n * pointer, and a click sends it off in a burst that decays back to a cruise.\n */\n\nimport * as React from \"react\"\n\nimport { usePointerTarget } from \"@/hooks/use-pointer-target\"\nimport { useRobotClock } from \"@/hooks/use-robot-motion\"\nimport { clamp, type Vec2 } from \"@/lib/robocn/kinematics\"\nimport { solveSpine, spineOutline, type SpineJoint, type SpinePose } from \"@/lib/robocn/spine\"\nimport {\n  aboutPoint,\n  capsulePath,\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/** Seconds a poked dart takes to decay back into the cruise. */\nconst DART = 1.1\n\nexport type FishBehavior = \"cruise\" | \"dart\" | \"hover\" | \"static\"\n\n/** The fish is drawn in side elevation; that is the camera it defaults to. */\nconst NATIVE_VIEW: RobotView = \"profile\"\n/** Where it swims in the frame. */\nconst CENTRE = 212\nconst DATUM = 78\n\n/** How far the camera pulls back so the machine still fits a frame that\n *  was drawn for one view. One in the view it was drawn in. */\nconst fits: Record<RobotView, number> = { plan: 0.72, front: 1, profile: 1, iso: 0.94 }\n\n/** The frame was drawn for the side elevation, where the fish lies along it.\n *  Seen from above it stands up the frame instead, so the camera moves too. */\nconst anchors: Partial<Record<RobotView, { x: number; y: number }>> = {\n  plan: { x: 132, y: 30 },\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\nexport interface RobotFishProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /** What it does when `phase` is not supplied. */\n  /** Where the camera stands. One fish, four projections. */\n  view?: RobotView\n  behavior?: FishBehavior\n  /** Controlled cycle fraction. Supplying it stops the internal clock. */\n  phase?: number\n  /** Tail beats per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  /** Seconds of offset, so a shoal of them breaks step. */\n  offset?: number\n  /** Peak body swing, 0–1. Omit and the behavior sets it. */\n  amplitude?: number\n  /** Wave crests along the body, 0.25–3. */\n  waves?: number\n  /** Steady turn, −1 nose down to 1 nose up. Omit and it follows the pointer. */\n  turn?: number\n  /** Links in the body, 4–24. */\n  segments?: number\n  /** Pectoral fin angle in degrees, −45..45. Omit and the behavior works them. */\n  fins?: number\n  /** Turn toward the pointer, and dart when clicked. */\n  interactive?: boolean\n  onDart?: () => void\n  size?: RobotSize | number\n  variant?: RobotVariant\n  /** The seabed line and its scatter. */\n  showGround?: boolean\n  label?: string\n}\n\n/** Body length in world units, and the half-width the hull is built from. */\nconst LENGTH = 150\nconst GIRTH = 25\n\nfunction RobotFish({\n  behavior = \"cruise\", phase, view = NATIVE_VIEW, speed = 1.1, animate = true, paused = false, offset = 0,\n  amplitude, waves, turn, segments = 16, fins,\n  interactive = true, onDart,\n  size = \"md\", variant = \"solid\", showGround = true, label,\n  color, accent, metal, dark, glow, grid, palette: paletteOverride, className, style,\n  onPointerDown, ...props\n}: RobotFishProps) {\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  // A poke is one burst: the tail loads up, the fish surges, and both decay\n  // back into whatever it was doing.\n  const [poked, setPoked] = React.useState<number | null>(null)\n  const since = poked === null ? Infinity : clock - poked\n  const burst = since >= 0 && since < DART ? Math.exp(-since * 3.4) : 0\n\n  const pointer = usePointerTarget(svgRef, {\n    enabled: interactive && turn === undefined && !paused,\n    within: \"element\",\n    persist: false,\n    toWorld: React.useCallback((unit: Vec2) => ({\n      x: clamp((unit.x - 0.5) * 2, -1, 1),\n      // Above the fish pulls its nose up, below sends it down.\n      y: clamp((0.5 - unit.y) * 2.4, -1, 1),\n    }), []),\n  })\n\n  const scripted = fishBehaviorPose(behavior, clock)\n  const cycle = controlled ? phase : clock * speed * scripted.rate + burst * 0.5\n  const pose = solveSpine({\n    segments,\n    length: LENGTH,\n    phase: cycle,\n    amplitude: clamp((amplitude ?? scripted.amplitude) + burst * 0.4, 0, 1),\n    waves: waves ?? scripted.waves,\n    // The beat belongs to the tail: that is what a body wave looks like on a fish.\n    taper: 0.82,\n    turn: clamp((turn ?? pointer.target?.y ?? scripted.turn) * 0.55, -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 at = (s: number) => pose.joints[Math.round(clamp(s, 0, 1) * (pose.joints.length - 1))]\n  const hull = spineOutline(pose, halfWidth)\n  const head = pose.head\n  const tail = pose.tail\n  const pectoral = finiteClamp(fins ?? scripted.fin, -45, 45, 0)\n  const state = behavior === \"static\" ? \"holding station\" : burst > 0.05 ? \"darting\" : behavior\n  // The drawing is a side elevation, so it goes through `wall` where it swims\n  // and comes out untouched from the side. A hull is round in section, which\n  // the one elevation could only imply: off-axis the body is a chain of tubes\n  // down the solved spine, each as thick as the hull is there.\n  const camera = robotCamera(view)\n  const offAxis = view !== NATIVE_VIEW\n  const fit = fits[view] ?? 1\n  const anchor = anchors[view] ?? { x: CENTRE, y: DATUM }\n  const face = aboutPoint(camera.wall(0, 90), anchor.x, anchor.y, 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 solver point — x along the nose axis, y up — `across` units out. */\n  const along = (p: { x: number; y: number }, across = 0) =>\n    camera.project(across, p.y, -p.x)\n\n\n  return (\n    <svg\n      ref={svgRef}\n      role=\"img\"\n      aria-label={`Robot fish, ${state}, ${viewNames[view] ?? viewNames.profile}`}\n      viewBox=\"0 0 260 160\"\n      width={width}\n      height={px(width * 0.615)}\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        setPoked(clock)\n        onDart?.()\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 80 H 248 M 130 14 V 146\" strokeDasharray=\"2 3\" />\n        </g>\n      )}\n      {showGround && (\n        <g stroke={palette.grid} opacity={0.45}>\n          <path d=\"M 14 150 H 246\" strokeWidth={0.6} strokeDasharray=\"6 5\" fill=\"none\" />\n          {[36, 92, 158, 214].map((x, index) => (\n            <circle key={x} cx={x} cy={px(146 - index * 1.5)} r={1.4} fill={palette.grid} stroke=\"none\" />\n          ))}\n        </g>\n      )}\n\n      {/* Solver coordinates: nose at the origin pointing along +x, y up. */}\n      {offAxis && <g data-solids transform={`translate(${anchor.x} ${anchor.y}) scale(${fit})`}>\n        {pose.joints.slice(0, -1).map((joint, index) => (\n          <path\n            key={joint.s}\n            data-segment={index}\n            d={capsulePath(along(joint.position), along(pose.joints[index + 1].position), px(Math.max(halfWidth(joint.s), 1)))}\n            {...shell}\n          />\n        ))}\n        <path d={capsulePath(along(head.position), along(pose.joints[1].position), px(Math.max(halfWidth(0.08), 2)))} {...cast} />\n      </g>}\n      <Frame {...frame}>\n      <g data-fish data-view={view} transform={`translate(${anchor.x} ${anchor.y}) scale(1 -1)`}>\n        <g data-tail transform={`translate(${px(tail.position.x)} ${px(tail.position.y)}) rotate(${px(tail.angle)})`}>\n          <path d=\"M 0 0 L -26 19 L -16 0 L -26 -19 Z\" {...machined} />\n          <path d=\"M -3 0 L -22 14 M -3 0 L -22 -14\" fill=\"none\" stroke={palette.dark} strokeWidth={0.8} opacity={0.7} />\n        </g>\n\n        <path data-fin=\"dorsal\" d={finPath(pose, 0.24, 0.5, 1, 13)} {...machined} />\n        <path data-fin=\"anal\" d={finPath(pose, 0.52, 0.7, -1, 8)} {...machined} />\n\n        <path data-spine d={hull} {...shell} />\n\n        {/* Plate seams: one per solved joint, so the hull reads as segments. */}\n        <g stroke={palette.dark} strokeWidth={0.7} opacity={0.45} fill=\"none\">\n          {pose.joints.slice(2, -1).map((joint, index) => (\n            <path key={index} d={seam(joint, 0.82)} />\n          ))}\n        </g>\n        <path\n          data-lateral\n          d={pose.joints\n            .map((joint, index) => `${index ? \"L\" : \"M\"} ${px(joint.position.x)} ${px(joint.position.y)}`)\n            .join(\" \")}\n          fill=\"none\"\n          stroke={palette.accent}\n          strokeWidth={1.2}\n          strokeLinecap=\"round\"\n          opacity={0.8}\n        />\n\n        <g data-fin=\"pectoral\" transform={`translate(${px(at(0.22).position.x)} ${px(at(0.22).position.y - 4)}) rotate(${px(at(0.22).angle + pectoral)})`}>\n          <path d=\"M 0 0 Q -11 -5 -19 -13 Q -9 -10 0 -4 Z\" {...cast} />\n        </g>\n\n        <g data-head transform={`translate(${px(head.position.x)} ${px(head.position.y)}) rotate(${px(head.angle)})`}>\n          <path d={`M 0 0 Q -10 ${px(halfWidth(0.06))} -22 ${px(halfWidth(0.15))} L -22 ${px(-halfWidth(0.15))} Q -10 ${px(-halfWidth(0.06))} 0 0 Z`} {...cast} />\n          <circle cx={-14} cy={7} r={4.6} {...machined} />\n          <circle cx={-14} cy={7} r={2.6} fill={palette.accent} />\n          <circle cx={-13} cy={7.9} r={1} fill={palette.dark} />\n          <path d=\"M -26 11 Q -31 0 -26 -11\" fill=\"none\" stroke={palette.metal} strokeWidth={1.4} />\n          <circle cx={-4} cy={-1} r={1.6} fill={palette.glow} opacity={0.9} />\n        </g>\n\n        {burst > 0.05 && (\n          <g data-wake opacity={px(burst * 0.5)}>\n            {[0, 1, 2].map((index) => (\n              <circle\n                key={index}\n                cx={px(tail.position.x - 12 - index * 11)}\n                cy={px(tail.position.y + (index % 2 ? 5 : -5))}\n                r={px(2.6 - index * 0.6)}\n                fill=\"none\"\n                stroke={palette.glow}\n                strokeWidth={1}\n              />\n            ))}\n          </g>\n        )}\n      </g>\n      </Frame>\n\n      {label && (\n        <text x={130} y={156} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\" fontSize={5.5} 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/**\n * The hull's half-width: a snout that opens quickly into the deepest section a\n * third of the way back, then a long taper to the peduncle the tail hangs off.\n */\nfunction halfWidth(s: number) {\n  const t = clamp(s, 0, 1)\n  // Opens fast behind the snout, deepest at the shoulder, then a long run out\n  // to the narrow peduncle the tail hangs off.\n  const nose = Math.min(1, Math.pow(t / 0.12, 0.55))\n  return GIRTH * nose * (0.1 + 0.9 * Math.pow(1 - t, 1.6))\n}\n\n/** A seam across the body at one joint, inset from the hull edge. */\nfunction seam(joint: SpineJoint, inset: number) {\n  const normal = ((joint.angle + 90) * Math.PI) / 180\n  const w = halfWidth(joint.s) * inset\n  const dx = Math.cos(normal) * w\n  const dy = Math.sin(normal) * w\n  const { x, y } = joint.position\n  return `M ${px(x + dx)} ${px(y + dy)} L ${px(x - dx)} ${px(y - dy)}`\n}\n\n/**\n * A fin riding the hull edge between two stations: a swept triangle whose base\n * sits on the solved outline and whose tip rakes back toward the tail.\n */\nfunction finPath(pose: SpinePose, from: number, to: number, side: 1 | -1, height: number) {\n  const last = pose.joints.length - 1\n  const pick = (s: number) => pose.joints[Math.round(clamp(s, 0, 1) * last)]\n  const edge = (joint: SpineJoint, out = 0) => {\n    const normal = ((joint.angle + 90) * Math.PI) / 180\n    const w = (halfWidth(joint.s) + out) * side\n    return {\n      x: joint.position.x + Math.cos(normal) * w,\n      y: joint.position.y + Math.sin(normal) * w,\n    }\n  }\n  const root = pick(from)\n  const a = edge(root, -1)\n  const b = edge(pick(to), -1)\n  // The tip stands off the leading half of the base and rakes toward the tail.\n  const tip = edge(pick(from + (to - from) * 0.35), height)\n  const along = (root.angle * Math.PI) / 180\n  const rake = (to - from) * 46\n  const apex = { x: tip.x - Math.cos(along) * rake, y: tip.y - Math.sin(along) * rake }\n  return [\n    `M ${px(a.x)} ${px(a.y)}`,\n    `Q ${px(apex.x)} ${px(apex.y)} ${px(b.x)} ${px(b.y)}`,\n    \"Z\",\n  ].join(\" \")\n}\n\n/** What the fish does with no timeline on it: beat, burst, or hold on fins. */\nexport function fishBehaviorPose(behavior: FishBehavior, clock: number) {\n  const time = Number.isFinite(clock) ? clock : 0\n  switch (behavior) {\n    // Burst and glide: a hard beat, then the body goes slack and coasts.\n    case \"dart\": {\n      const surge = Math.pow(Math.max(0, Math.sin(time * 0.85)), 3)\n      return { amplitude: 0.12 + 0.72 * surge, waves: 1.25, rate: 0.6 + surge * 1.9, turn: 0, fin: -12 + surge * 20 }\n    }\n    // Holding station is fin work, with the body barely moving.\n    case \"hover\":\n      return { amplitude: 0.14, waves: 0.7, rate: 0.55, turn: 0, fin: 26 * Math.sin(time * 2.2) }\n    case \"static\":\n      return { amplitude: 0.45, waves: 1.2, rate: 1, turn: 0, fin: 0 }\n    default:\n      return { amplitude: 0.5, waves: 1.2, rate: 1, turn: 0, fin: 8 * Math.sin(time * 0.9) }\n  }\n}\n\nexport { RobotFish }\n",
      "type": "registry:ui",
      "target": "@ui/robot-fish.tsx"
    }
  ],
  "categories": [
    "robotics",
    "animals"
  ],
  "type": "registry:ui"
}