{
  "name": "player",
  "title": "Player",
  "type": "registry:component",
  "description": "First/third-person character controller (ecctrl wrapper). WASD moves; mouse-drag or the arrow keys steer the camera in either view. Reads the world's `avatar` setting for its default view and the world's `controls` for key bindings.",
  "dependencies": [
    "@react-three/drei@^10.7.7",
    "@react-three/fiber@^9.6.1",
    "@runek/core@^0.12.0",
    "ecctrl@^1.0.97",
    "three@^0.184.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "Player.tsx",
      "content": "import { useKeyboardControls } from '@react-three/drei'\nimport { useFrame, useThree } from '@react-three/fiber'\nimport { type AvatarView, useWorld, type Vec3 } from '@runek/core'\nimport Ecctrl from 'ecctrl'\nimport { type ReactNode, useRef } from 'react'\nimport type { Object3D } from 'three'\n\nexport type PlayerView = AvatarView\n\n// Keyboard look. ecctrl only moves its camera from mouse/touch/gamepad — there are no key\n// actions for it — so we drive its camera rig ourselves from the `turnLeft`/`turnRight` and\n// `lookUp`/`lookDown` actions in the world's controls (see `DEFAULT_CONTROLS` in `@runek/core`).\n// Yaw is the pivot's `rotation.y`; pitch is the follow-cam's `rotation.x` plus a matching\n// reposition along a vertical arc, exactly as ecctrl's own mouse handler does — the same math\n// ecctrl runs in both views, so this works first- and third-person alike. ecctrl writes these\n// only on pointer input (never per frame in CameraBasedMovement), so our additions compose\n// cleanly with mouse-drag. A world whose controls omit these actions leaves the camera entirely\n// to the mouse.\nconst TURN_SPEED = 1.8 // yaw, radians/second\nconst LOOK_SPEED = 1.2 // pitch, radians/second\n// Pitch clamp — mirrors ecctrl's camLowLimit / camUpLimit defaults (we pass neither to Ecctrl).\nconst CAM_LOW = -1.3\nconst CAM_UP = 1.5\n\nfunction CameraKeyLook() {\n  const scene = useThree((s) => s.scene)\n  const [, getKeys] = useKeyboardControls()\n  const pivot = useRef<Object3D | null>(null)\n\n  useFrame((_, dt) => {\n    // ecctrl's camera pivot is a bare Object3D added to the scene whose single child is the\n    // (childless) follow-cam, offset back along z by the camera distance. Match on that shape\n    // and cache; until it mounts (or if the match ever misses) mouse-drag still turns, so a\n    // miss just no-ops rather than breaking.\n    if (!pivot.current) {\n      pivot.current =\n        scene.children.find(\n          (o) =>\n            o.type === 'Object3D' &&\n            o.children.length === 1 &&\n            o.children[0].type === 'Object3D' &&\n            o.children[0].children.length === 0 &&\n            // The follow-cam sits offset back along -z (the third-person camera\n            // distance, or -0.01 in first person). Sign + shape identify ecctrl's\n            // pivot in both views without tying keyboard look to one of them.\n            o.children[0].position.x === 0 &&\n            o.children[0].position.z < -0.001,\n        ) ?? null\n      if (!pivot.current) return\n    }\n    const step = Math.min(dt, 0.05)\n    const keys = getKeys() as Record<string, boolean>\n\n    // Yaw. Match mouse-drag's sign: dragging right decrements rotation.y, so ArrowRight does too.\n    const turn = (keys.turnLeft ? 1 : 0) - (keys.turnRight ? 1 : 0)\n    if (turn) pivot.current.rotation.y += turn * TURN_SPEED * step\n\n    // Pitch. ArrowUp lowers the follow-cam to look up; ArrowDown raises it to look down. Repeat\n    // ecctrl's mouse-pitch math on the follow-cam so the camera orbits the same vertical arc.\n    const pitch = (keys.lookDown ? 1 : 0) - (keys.lookUp ? 1 : 0)\n    if (pitch) {\n      const cam = pivot.current.children[0]\n      const vy = Math.min(Math.max(cam.rotation.x + pitch * LOOK_SPEED * step, CAM_LOW), CAM_UP)\n      const dist = cam.position.length()\n      cam.rotation.x = vy\n      cam.position.y = -dist * Math.sin(-vy)\n      cam.position.z = -dist * Math.cos(-vy)\n    }\n  })\n  return null\n}\n\nexport interface PlayerProps {\n  position?: Vec3\n  /** Camera view. Unset defers to the world default (`<World avatar>`); falls back\n   *  to first-person. An explicit value here always wins. */\n  view?: PlayerView\n  /** Initial camera yaw in radians (0 faces +z). */\n  yaw?: number\n  /** Custom avatar visual, replacing the default capsule. Size it to the capsule\n   *  envelope (~1.3 units tall, centered at the character origin); it is hidden in\n   *  first-person view. In world JSON, nest it as a child node of the Player. */\n  children?: ReactNode\n}\n\nconst CAPSULE_RADIUS = 0.3\nconst CAPSULE_HALF_HEIGHT = 0.35\n\nexport function Player({ position = [0, 3, 0], view, yaw = 0, children }: PlayerProps) {\n  const { avatar } = useWorld()\n  const firstPerson = (view ?? avatar ?? 'first') === 'first'\n\n  return (\n    <Ecctrl\n      position={position}\n      mode=\"CameraBasedMovement\"\n      camInitDir={{ x: 0, y: yaw }}\n      capsuleRadius={CAPSULE_RADIUS}\n      capsuleHalfHeight={CAPSULE_HALF_HEIGHT}\n      camInitDis={firstPerson ? -0.01 : -5}\n      camMinDis={firstPerson ? -0.01 : -1.5}\n      camMaxDis={firstPerson ? -0.01 : -8}\n      turnVelMultiplier={firstPerson ? 1 : 0.2}\n      turnSpeed={firstPerson ? 100 : 15}\n      camFollowMult={firstPerson ? 1000 : 11}\n      camLerpMult={firstPerson ? 1000 : 25}\n    >\n      <group visible={!firstPerson}>\n        {children ?? (\n          <mesh castShadow>\n            <capsuleGeometry args={[CAPSULE_RADIUS, CAPSULE_HALF_HEIGHT * 2, 8, 16]} />\n            <meshStandardMaterial color=\"#4a90d9\" />\n          </mesh>\n        )}\n      </group>\n      <CameraKeyLook />\n    </Ecctrl>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
