{
  "name": "portal",
  "title": "Portal",
  "type": "registry:component",
  "description": "A travel gate: a glowing sensor ring that fires onEnter (or navigates to a destination) when the avatar or a vehicle passes through — for world-to-world travel, level transitions, and teleports.",
  "dependencies": [
    "@react-three/fiber@^9.6.1",
    "@react-three/rapier@^2.2.0",
    "@runek/core@^0.12.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [
    "sign"
  ],
  "files": [
    {
      "path": "Portal.tsx",
      "content": "import { useFrame } from '@react-three/fiber'\nimport { CuboidCollider, RigidBody } from '@react-three/rapier'\nimport { useWorld, type WorldComponentProps } from '@runek/core'\nimport { useRef } from 'react'\nimport type { Group, MeshStandardMaterial } from 'three'\nimport { Sign } from './Sign'\n\nexport interface PortalProps extends WorldComponentProps {\n  /**\n   * Where the portal leads — a URL or app route. Kept JSON-serializable so a world\n   * round-trips from data. With no `onEnter`, entering navigates here\n   * (`window.location.href = to`); with `onEnter`, this is just passed through to it.\n   */\n  to?: string\n  /**\n   * Called once when the avatar (or a vehicle) enters the gate, with `to`. Optional, so\n   * the component still renders and round-trips from data without it (CONTRACT §1). Use it\n   * to drive an in-app transition instead of a full navigation — e.g. swap the mounted world.\n   * Note: a host rendered inside `<WorldRenderer>` lives in a separate React reconciler from\n   * the page, so bridge this callback out through a module-level store, not React context.\n   */\n  onEnter?: (to: string | undefined) => void\n  /** Floating caption above the gate (e.g. the destination's name). */\n  label?: string\n  /** Ring radius, in units. */\n  radius?: number\n  /** Glow / ring color; defaults to the world palette's `accent` slot. */\n  color?: string\n  /** Arm the trigger. Set false for a decorative or not-yet-open gate. Default true. */\n  active?: boolean\n}\n\n// rapier RigidBody types: 0 Dynamic, 1 Fixed, 2 KinematicPosition, 3 KinematicVelocity.\nconst FIXED = 1\n// `ActiveCollisionTypes`: DEFAULT (15) covers a dynamic body (the on-foot avatar) vs anything;\n// OR in KINEMATIC_FIXED (8704) so the gate also fires for a kinematic vehicle (a boat, a cart)\n// against this fixed sensor. The avatar capsule is dynamic; a scripted vehicle is kinematic.\nconst TRIGGER_TYPES = 15 | 8704\n\n/**\n * A travel gate: a glowing ring that fires a one-shot event when the avatar — or a vehicle —\n * passes through it. The visual is procedural and asset-free; the trigger is a Rapier sensor.\n * Drive transitions with the optional `onEnter` callback, or set `to` and let it navigate.\n * Use it for world-to-world travel, level/hub transitions, or in-world teleports; for a single\n * continuous space, move the camera instead.\n */\nexport function Portal({\n  to,\n  onEnter,\n  label,\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  radius = 2.2,\n  color,\n  active = true,\n}: PortalProps) {\n  const { unit, palette } = useWorld()\n  const ink = color ?? palette.accent\n  const R = radius * unit\n\n  const ring = useRef<Group>(null)\n  const ringMat = useRef<MeshStandardMaterial>(null)\n\n  useFrame((state) => {\n    const t = state.clock.elapsedTime\n    if (ring.current) ring.current.rotation.y = t * 0.6\n    if (ringMat.current) ringMat.current.emissiveIntensity = 1.4 + Math.sin(t * 2) * 0.5\n  })\n\n  return (\n    <RigidBody type=\"fixed\" colliders={false} position={position} rotation={rotation}>\n      {/* A generous sensor around the gate; deep enough on the approach axis that a fast\n          vehicle can't tunnel through between frames. Ignores Fixed bodies (terrain, props),\n          so it never fires against the ground it stands on. */}\n      <CuboidCollider\n        sensor\n        activeCollisionTypes={TRIGGER_TYPES}\n        args={[R * 1.1, R + 0.6 * unit, Math.max(R, 1.6 * unit)]}\n        position={[0, R, 0]}\n        onIntersectionEnter={({ other }) => {\n          if (!active || !other.rigidBody || other.rigidBody.bodyType() === FIXED) return\n          if (onEnter) onEnter(to)\n          else if (to) window.location.href = to\n        }}\n      />\n\n      {/* Spinning glow ring, standing on the surface and facing the approach (local ±Z). */}\n      <group ref={ring} position={[0, R, 0]}>\n        <mesh castShadow>\n          <torusGeometry args={[R, 0.12 * R, 14, 56]} />\n          <meshStandardMaterial\n            ref={ringMat}\n            color={ink}\n            emissive={ink}\n            emissiveIntensity={1.4}\n            roughness={0.35}\n            metalness={0.1}\n          />\n        </mesh>\n      </group>\n\n      {/* Soft beam rising from the ring — a no-bloom glow marker visible from afar. */}\n      <mesh position={[0, R * 1.4, 0]}>\n        <cylinderGeometry args={[R * 0.82, R * 0.82, R * 2.8, 24, 1, true]} />\n        <meshBasicMaterial color={ink} transparent opacity={0.12} depthWrite={false} />\n      </mesh>\n\n      {label && (\n        <Sign position={[0, R * 2 + 0.9 * unit, 0]} size={0.7} color={ink} glow>\n          {label}\n        </Sign>\n      )}\n    </RigidBody>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
