diff --git a/src/behaviours/joystick-dpad4.js b/src/behaviours/joystick-dpad4.js
index d0cc1e2a87d1882346deb724d5ba14570d46b02c..0eae7e1d194dfb6f207da53a7499caaf8e483c02 100644
--- a/src/behaviours/joystick-dpad4.js
+++ b/src/behaviours/joystick-dpad4.js
@@ -8,10 +8,16 @@ function joystick_dpad4(el, outputPrefix) {
   this.previous = "none";
   this.hapticIntensity = "low";
   this.emitDPad4 = this.emitDPad4.bind(this);
-  el.addEventListener("axismove", this.emitDPad4);
+  this.el = el;
 }
 
 joystick_dpad4.prototype = {
+  addEventListeners: function() {
+    this.el.addEventListener("axismove", this.emitDPad4);
+  },
+  removeEventListeners: function() {
+    this.el.removeEventListener("axismove", this.emitDPad4);
+  },
   emitDPad4: function(event) {
     const x = event.detail.axis[0];
     const y = event.detail.axis[1];
diff --git a/src/behaviours/trackpad-dpad4.js b/src/behaviours/trackpad-dpad4.js
index b23ca1dbdb1f61ec6d99e9d2fab17ab4c96b0248..f15908d34d5854d60ae158af7026007745293dfb 100644
--- a/src/behaviours/trackpad-dpad4.js
+++ b/src/behaviours/trackpad-dpad4.js
@@ -10,12 +10,20 @@ function trackpad_dpad4(el, outputPrefix) {
   this.unpress = this.unpress.bind(this);
   this.hapticIntensity = "low";
   this.centerRadius = 0.6;
-  el.addEventListener("axismove", this.emitDPad4);
-  el.addEventListener("trackpaddown", this.press);
-  el.addEventListener("trackpadup", this.unpress);
+  this.el = el;
 }
 
 trackpad_dpad4.prototype = {
+  addEventListeners: function() {
+    this.el.addEventListener("axismove", this.emitDPad4);
+    this.el.addEventListener("trackpaddown", this.press);
+    this.el.addEventListener("trackpadup", this.unpress);
+  },
+  removeEventListeners: function() {
+    this.el.removeEventListener("axismove", this.emitDPad4);
+    this.el.removeEventListener("trackpaddown", this.press);
+    this.el.removeEventListener("trackpadup", this.unpress);
+  },
   press: function() {
     this.pressed = true;
   },
diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js
new file mode 100644
index 0000000000000000000000000000000000000000..572b17d91f606912b612fb3e1b8e440f33be46a0
--- /dev/null
+++ b/src/components/cursor-controller.js
@@ -0,0 +1,445 @@
+const TARGET_TYPE_NONE = 1;
+const TARGET_TYPE_INTERACTABLE = 2;
+const TARGET_TYPE_UI = 4;
+const TARGET_TYPE_INTERACTABLE_OR_UI = TARGET_TYPE_INTERACTABLE | TARGET_TYPE_UI;
+
+AFRAME.registerComponent("cursor-controller", {
+  dependencies: ["raycaster", "line"],
+  schema: {
+    cursor: { type: "selector" },
+    camera: { type: "selector" },
+    playerRig: { type: "selector" },
+    gazeTeleportControls: { type: "selector" },
+    physicalHandSelector: { type: "string" },
+    handedness: { default: "right", oneOf: ["right", "left"] },
+    maxDistance: { default: 3 },
+    minDistance: { default: 0.5 },
+    cursorColorHovered: { default: "#2F80ED" },
+    cursorColorUnhovered: { default: "#FFFFFF" },
+    primaryDown: { default: "action_primary_down" },
+    primaryUp: { default: "action_primary_up" },
+    grabEvent: { default: "action_grab" },
+    releaseEvent: { default: "action_release" }
+  },
+
+  init: function() {
+    this.inVR = false;
+    this.isMobile = AFRAME.utils.device.isMobile();
+    this.hasPointingDevice = false;
+    this.currentTargetType = TARGET_TYPE_NONE;
+    this.grabStarting = false;
+    this.currentDistance = this.data.maxDistance;
+    this.currentDistanceMod = 0;
+    this.mousePos = new THREE.Vector2();
+    this.controller = null;
+    this.controllerQueue = [];
+    this.wasCursorHovered = false;
+    this.wasPhysicalHandGrabbing = false;
+    this.origin = new THREE.Vector3();
+    this.direction = new THREE.Vector3();
+    this.controllerQuaternion = new THREE.Quaternion();
+
+    this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
+
+    this._handleMouseDown = this._handleMouseDown.bind(this);
+    this._handleMouseMove = this._handleMouseMove.bind(this);
+    this._handleMouseUp = this._handleMouseUp.bind(this);
+    this._handleWheel = this._handleWheel.bind(this);
+    this._handleEnterVR = this._handleEnterVR.bind(this);
+    this._handleExitVR = this._handleExitVR.bind(this);
+    this._handlePrimaryDown = this._handlePrimaryDown.bind(this);
+    this._handlePrimaryUp = this._handlePrimaryUp.bind(this);
+    this._handleModelLoaded = this._handleModelLoaded.bind(this);
+    this._handleCursorLoaded = this._handleCursorLoaded.bind(this);
+    this._handleControllerConnected = this._handleControllerConnected.bind(this);
+    this._handleControllerDisconnected = this._handleControllerDisconnected.bind(this);
+
+    this._handleTouchStart = this._handleTouchStart.bind(this);
+    this._updateRaycasterIntersections = this._updateRaycasterIntersections.bind(this);
+    this._handleTouchMove = this._handleTouchMove.bind(this);
+    this._handleTouchEnd = this._handleTouchEnd.bind(this);
+
+    this.el.sceneEl.renderer.sortObjects = true;
+    this.data.cursor.addEventListener("loaded", this.cursorLoadedListener);
+  },
+
+  remove: function() {
+    this.data.cursor.removeEventListener("loaded", this._cursorLoadedListener);
+  },
+
+  update: function(oldData) {
+    if (oldData.physicalHand !== this.data.physicalHandSelector) {
+      this._handleModelLoaded();
+    }
+
+    if (oldData.handedness !== this.data.handedness) {
+      //TODO
+    }
+  },
+
+  play: function() {
+    if (!this.inVR && this.isMobile && !this.hasPointingDevice) {
+      document.addEventListener("touchstart", this._handleTouchStart);
+      document.addEventListener("touchmove", this._handleTouchMove);
+      document.addEventListener("touchend", this._handleTouchEnd);
+    } else {
+      document.addEventListener("mousedown", this._handleMouseDown);
+      document.addEventListener("mousemove", this._handleMouseMove);
+      document.addEventListener("mouseup", this._handleMouseUp);
+      document.addEventListener("wheel", this._handleWheel);
+    }
+
+    window.addEventListener("enter-vr", this._handleEnterVR);
+    window.addEventListener("exit-vr", this._handleExitVR);
+
+    this.data.playerRig.addEventListener(this.data.primaryDown, this._handlePrimaryDown);
+    this.data.playerRig.addEventListener(this.data.primaryUp, this._handlePrimaryUp);
+    this.data.playerRig.addEventListener(this.data.grabEvent, this._handlePrimaryDown);
+    this.data.playerRig.addEventListener(this.data.releaseEvent, this._handlePrimaryUp);
+
+    this.data.playerRig.addEventListener("model-loaded", this._handleModelLoaded);
+
+    this.el.sceneEl.addEventListener("controllerconnected", this._handleControllerConnected);
+    this.el.sceneEl.addEventListener("controllerdisconnected", this._handleControllerDisconnected);
+  },
+
+  pause: function() {
+    document.removeEventListener("touchstart", this._handleTouchStart);
+    document.removeEventListener("touchmove", this._handleTouchMove);
+    document.removeEventListener("touchend", this._handleTouchEnd);
+    document.removeEventListener("mousedown", this._handleMouseDown);
+    document.removeEventListener("mousemove", this._handleMouseMove);
+    document.removeEventListener("mouseup", this._handleMouseUp);
+    document.removeEventListener("wheel", this._handleWheel);
+
+    window.removeEventListener("enter-vr", this._handleEnterVR);
+    window.removeEventListener("exit-vr", this._handleExitVR);
+
+    this.data.playerRig.removeEventListener(this.data.primaryDown, this._handlePrimaryDown);
+    this.data.playerRig.removeEventListener(this.data.primaryUp, this._handlePrimaryUp);
+    this.data.playerRig.removeEventListener(this.data.grabEvent, this._handlePrimaryDown);
+    this.data.playerRig.removeEventListener(this.data.releaseEvent, this._handlePrimaryUp);
+
+    this.data.playerRig.removeEventListener("model-loaded", this._handleModelLoaded);
+
+    this.el.sceneEl.removeEventListener("controllerconnected", this._handleControllerConnected);
+    this.el.sceneEl.removeEventListener("controllerdisconnected", this._handleControllerDisconnected);
+  },
+
+  tick: function() {
+    //handle physical hand
+    if (this.physicalHand) {
+      const state = this.physicalHand.components["super-hands"].state;
+      const isPhysicalHandGrabbing = state.has("grab-start") || state.has("hover-start");
+      if (this.wasPhysicalHandGrabbing != isPhysicalHandGrabbing) {
+        this._setCursorVisibility(!isPhysicalHandGrabbing);
+        this.currentTargetType = TARGET_TYPE_NONE;
+      }
+      this.wasPhysicalHandGrabbing = isPhysicalHandGrabbing;
+      if (isPhysicalHandGrabbing) return;
+    }
+
+    //set raycaster origin/direction
+    const camera = this.data.camera.components.camera.camera;
+    if (!this.inVR) {
+      //mouse cursor mode
+      const raycaster = this.el.components.raycaster.raycaster;
+      raycaster.setFromCamera(this.mousePos, camera);
+      this.origin.copy(raycaster.ray.origin);
+      this.direction.copy(raycaster.ray.direction);
+    } else if ((this.inVR || this.isMobile) && !this.hasPointingDevice) {
+      //gaze cursor mode
+      camera.getWorldPosition(this.origin);
+      camera.getWorldDirection(this.direction);
+    } else if (this.controller != null) {
+      //3d cursor mode
+      this.controller.object3D.getWorldPosition(this.origin);
+      this.controller.object3D.getWorldQuaternion(this.controllerQuaternion);
+      this.direction
+        .set(0, 0, -1)
+        .applyQuaternion(this.controllerQuaternion)
+        .normalize();
+    }
+
+    this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction });
+
+    let intersection = null;
+
+    //update cursor position
+    if (!this._isGrabbing()) {
+      const intersections = this.el.components.raycaster.intersections;
+      if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) {
+        intersection = intersections[0];
+        this.data.cursor.object3D.position.copy(intersection.point);
+        this.currentDistance = intersections[0].distance;
+      } else {
+        this.currentDistance = this.data.maxDistance;
+      }
+      this.currentDistanceMod = 0;
+    }
+
+    if (this._isGrabbing() || !intersection) {
+      const max = Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod);
+      const distance = Math.min(max, this.data.maxDistance);
+      this.currentDistanceMod = this.currentDistance - distance;
+      this.direction.multiplyScalar(distance);
+      this.data.cursor.object3D.position.addVectors(this.origin, this.direction);
+    }
+
+    //update currentTargetType
+    if (this._isGrabbing() && !intersection) {
+      this.currentTargetType = TARGET_TYPE_INTERACTABLE;
+    } else if (intersection) {
+      if (intersection.object.el.matches(".interactable, .interactable *")) {
+        this.currentTargetType = TARGET_TYPE_INTERACTABLE;
+      } else if (intersection.object.el.matches(".ui, .ui *")) {
+        this.currentTargetType = TARGET_TYPE_UI;
+      }
+    } else {
+      this.currentTargetType = TARGET_TYPE_NONE;
+    }
+
+    //update cursor material
+    const isTarget = this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI);
+    if ((this._isGrabbing() || isTarget) && !this.wasCursorHovered) {
+      this.wasCursorHovered = true;
+      this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered });
+    } else if (!this._isGrabbing() && !isTarget && this.wasCursorHovered) {
+      this.wasCursorHovered = false;
+      this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
+    }
+
+    //update line
+    if (this.hasPointingDevice) {
+      this.el.setAttribute("line", { start: this.origin.clone(), end: this.data.cursor.object3D.position.clone() });
+    }
+  },
+
+  _isGrabbing() {
+    return this.data.cursor.components["super-hands"].state.has("grab-start");
+  },
+
+  _isTargetOfType: function(mask) {
+    return (this.currentTargetType & mask) === this.currentTargetType;
+  },
+
+  _setCursorVisibility(visible) {
+    this.data.cursor.setAttribute("visible", visible);
+    this.el.setAttribute("line", { visible: visible && this.hasPointingDevice });
+  },
+
+  _setLookControlsEnabled(enabled) {
+    const lookControls = this.data.camera.components["look-controls"];
+    if (lookControls) {
+      if (enabled) {
+        lookControls.play();
+      } else {
+        lookControls.pause();
+      }
+    }
+  },
+
+  _startTeleport: function() {
+    if (this.controller != null) {
+      this.controller.emit("cursor-teleport_down", {});
+    } else if (this.inVR) {
+      this.data.gazeTeleportControls.emit("cursor-teleport_down", {});
+    }
+    this._setCursorVisibility(false);
+  },
+
+  _endTeleport: function() {
+    if (this.controller != null) {
+      this.controller.emit("cursor-teleport_up", {});
+    } else if (this.inVR) {
+      this.data.gazeTeleportControls.emit("cursor-teleport_up", {});
+    }
+    this._setCursorVisibility(true);
+  },
+
+  _handleTouchStart: function(e) {
+    const touch = e.touches[0];
+    if (touch.clientY / window.innerHeight >= 0.8) return true;
+    this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1);
+    this._updateRaycasterIntersections();
+
+    // update cursor position
+    if (!this.isGrabbing) {
+      const intersections = this.el.components.raycaster.intersections;
+      if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) {
+        const intersection = intersections[0];
+        this.data.cursor.object3D.position.copy(intersection.point);
+        this.currentDistance = intersections[0].distance;
+        this.currentDistanceMod = 0;
+      } else {
+        this.currentDistance = this.data.maxDistance;
+      }
+    }
+
+    this._setLookControlsEnabled(false);
+
+    // Set timeout because if I don't, the duck moves is picked up at the
+    // the wrong offset from the cursor: If the cursor started below and
+    // to the left, the duck lifts above and to the right by the same amount.
+    // I don't understand exactly why this is, since I am setting the
+    // cursor object's position manually in this function, but something else
+    // must happen before cursor-grab ends up doing the right thing.
+    // TODO : Figure this out.
+    window.setTimeout(() => {
+      this.data.cursor.emit("cursor-grab", {});
+    }, 40);
+
+    this.lastTouch = touch;
+  },
+
+  _updateRaycasterIntersections: function() {
+    const raycaster = this.el.components.raycaster.raycaster;
+    const camera = this.data.camera.components.camera.camera;
+    raycaster.setFromCamera(this.mousePos, camera);
+    this.origin = raycaster.ray.origin;
+    this.direction = raycaster.ray.direction;
+    this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction });
+    this.el.components.raycaster.checkIntersections();
+  },
+
+  _handleTouchMove: function(e) {
+    for (let i = 0; i < e.touches.length; i++) {
+      const touch = e.touches[i];
+      if (touch.clientY / window.innerHeight >= 0.8) return true;
+      this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1);
+      this.lastTouch = touch;
+    }
+  },
+
+  _handleTouchEnd: function(e) {
+    for (let i = 0; i < e.changedTouches.length; i++) {
+      const touch = e.changedTouches[i];
+      const thisTouchDidNotDriveMousePos =
+        Math.abs(touch.clientX - this.lastTouch.clientX) > 0.1 &&
+        Math.abs(touch.clientY - this.lastTouch.clientY) > 0.1;
+      if (thisTouchDidNotDriveMousePos) {
+        return;
+      }
+    }
+    this._setLookControlsEnabled(true);
+    this.data.cursor.emit("cursor-release", {});
+  },
+
+  _handleMouseDown: function() {
+    if (this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI)) {
+      this._setLookControlsEnabled(false);
+      this.data.cursor.emit("cursor-grab", {});
+    } else if (this.inVR || this.isMobile) {
+      this._startTeleport();
+    }
+  },
+
+  _handleMouseMove: function(e) {
+    this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
+  },
+
+  _handleMouseUp: function() {
+    this._setLookControlsEnabled(true);
+    this.data.cursor.emit("cursor-release", {});
+    this._endTeleport();
+  },
+
+  _handleWheel: function(e) {
+    if (this._isGrabbing()) {
+      switch (e.deltaMode) {
+        case e.DOM_DELTA_PIXEL:
+          this.currentDistanceMod += e.deltaY / 500;
+          break;
+        case e.DOM_DELTA_LINE:
+          this.currentDistanceMod += e.deltaY / 10;
+          break;
+        case e.DOM_DELTA_PAGE:
+          this.currentDistanceMod += e.deltaY / 2;
+          break;
+      }
+    }
+  },
+
+  _handleEnterVR: function() {
+    if (AFRAME.utils.device.checkHeadsetConnected()) {
+      this.inVR = true;
+      this._updateController();
+    }
+  },
+
+  _handleExitVR: function() {
+    this.inVR = false;
+    this._updateController();
+  },
+
+  _handlePrimaryDown: function(e) {
+    if (e.target === this.controller) {
+      const isInteractable = this._isTargetOfType(TARGET_TYPE_INTERACTABLE) && !this.grabStarting;
+      if (isInteractable || this._isTargetOfType(TARGET_TYPE_UI)) {
+        this.grabStarting = true;
+        this.data.cursor.emit("cursor-grab", e.detail);
+      } else if (e.type !== this.data.grabEvent) {
+        this._startTeleport();
+      }
+    }
+  },
+
+  _handlePrimaryUp: function(e) {
+    if (e.target === this.controller) {
+      if (this._isGrabbing() || this._isTargetOfType(TARGET_TYPE_UI)) {
+        this.grabStarting = false;
+        this.data.cursor.emit("cursor-release", e.detail);
+      } else if (e.type !== this.data.releaseEvent) {
+        this._endTeleport();
+      }
+    }
+  },
+
+  _handleModelLoaded: function() {
+    this.physicalHand = this.data.playerRig.querySelector(this.data.physicalHandSelector);
+  },
+
+  _handleCursorLoaded: function() {
+    this.data.cursor.object3DMap.mesh.renderOrder = 1;
+  },
+
+  _handleControllerConnected: function(e) {
+    const data = {
+      controller: e.target,
+      handedness: e.detail.component.data.hand
+    };
+
+    if (data.handedness === this.data.handedness) {
+      this.controllerQueue.unshift(data);
+    } else {
+      this.controllerQueue.push(data);
+    }
+
+    this._updateController();
+  },
+
+  _handleControllerDisconnected: function(e) {
+    for (let i = 0; i < this.controllerQueue.length; i++) {
+      if (e.target === this.controllerQueue[i].controller) {
+        this.controllerQueue.splice(i, 1);
+        this._updateController();
+        return;
+      }
+    }
+  },
+
+  _updateController: function() {
+    this.hasPointingDevice = this.controllerQueue.length > 0 && this.inVR;
+
+    this._setCursorVisibility(this.hasPointingDevice);
+
+    if (this.hasPointingDevice) {
+      const controllerData = this.controllerQueue[0];
+      const hand = controllerData.handedness;
+      this.el.setAttribute("cursor-controller", { physicalHand: `#${hand}-super-hand` });
+      this.controller = controllerData.controller;
+    } else {
+      this.controller = null;
+    }
+  }
+});
diff --git a/src/components/event-repeater.js b/src/components/event-repeater.js
index 64e28720698d8aaa26a8f0ce1525301cebd1ee2f..e5e5ffd192b2b170a4efeac2e0c30e9ee43dd92a 100644
--- a/src/components/event-repeater.js
+++ b/src/components/event-repeater.js
@@ -24,6 +24,6 @@ AFRAME.registerComponent("event-repeater", {
   },
 
   _handleEvent: function(event, e) {
-    this.el.emit(event, e.details);
+    this.el.emit(event, e.detail);
   }
 });
diff --git a/src/components/super-cursor.js b/src/components/super-cursor.js
deleted file mode 100644
index ee70e960663144709f17108d70a2378a6bc9f935..0000000000000000000000000000000000000000
--- a/src/components/super-cursor.js
+++ /dev/null
@@ -1,168 +0,0 @@
-AFRAME.registerComponent("super-cursor", {
-  dependencies: ["raycaster"],
-  schema: {
-    cursor: { type: "selector" },
-    camera: { type: "selector" },
-    maxDistance: { default: 3 },
-    minDistance: { default: 0.5 },
-    cursorColorHovered: { default: "#FF0000" },
-    cursorColorUnhovered: { efault: "#FFFFFF" }
-  },
-
-  init: function() {
-    this.isGrabbing = false;
-    this.isInteractable = false;
-    this.wasIntersecting = false;
-    this.currentDistance = this.data.maxDistance;
-    this.currentDistanceMod = 0;
-    this.enabled = true;
-    this.origin = new THREE.Vector3();
-    this.direction = new THREE.Vector3();
-    this.point = new THREE.Vector3();
-    this.mousePos = new THREE.Vector2();
-
-    this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
-
-    this.mouseDownListener = this._handleMouseDown.bind(this);
-    this.mouseMoveListener = this._handleMouseMove.bind(this);
-    this.mouseUpListener = this._handleMouseUp.bind(this);
-    this.wheelListener = this._handleWheel.bind(this);
-    this.enterVRListener = this._handleEnterVR.bind(this);
-    this.exitVRListener = this._handleExitVR.bind(this);
-  },
-
-  play: function() {
-    document.addEventListener("mousedown", this.mouseDownListener);
-    document.addEventListener("mousemove", this.mouseMoveListener);
-    document.addEventListener("mouseup", this.mouseUpListener);
-    document.addEventListener("wheel", this.wheelListener);
-    window.addEventListener("enter-vr", this.enterVRListener);
-    window.addEventListener("exit-vr", this.exitVRListener);
-
-    this._enable();
-  },
-
-  pause: function() {
-    document.removeEventListener("mousedown", this.mouseDownListener);
-    document.removeEventListener("mousemove", this.mouseMoveListener);
-    document.removeEventListener("mouseup", this.mouseUpListener);
-    document.removeEventListener("wheel", this.wheelListener);
-    window.removeEventListener("enter-vr", this.enterVRListener);
-    window.removeEventListener("exit-vr", this.exitVRListener);
-
-    this._disable();
-  },
-
-  tick: function() {
-    if (!this.enabled) {
-      return;
-    }
-
-    this.isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start");
-
-    const camera = this.data.camera.components.camera.camera;
-    const raycaster = this.el.components.raycaster.raycaster;
-    raycaster.setFromCamera(this.mousePos, camera);
-    this.origin = raycaster.ray.origin;
-    this.direction = raycaster.ray.direction;
-    this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction });
-
-    let intersection = null;
-
-    if (!this.isGrabbing) {
-      const intersections = this.el.components.raycaster.intersections;
-      if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) {
-        intersection = intersections[0];
-        this.data.cursor.object3D.position.copy(intersection.point);
-        this.currentDistance = intersections[0].distance;
-        this.currentDistanceMod = 0;
-      } else {
-        this.currentDistance = this.data.maxDistance;
-      }
-    }
-
-    if (this.isGrabbing || !intersection) {
-      const distance = Math.min(
-        Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod),
-        this.data.maxDistance
-      );
-      this.currentDistanceMod = this.currentDistance - distance;
-      this.direction.multiplyScalar(distance);
-      this.point.addVectors(this.origin, this.direction);
-      this.data.cursor.object3D.position.copy(this.point);
-    }
-
-    this.isInteractable = intersection && this._isInteractable(intersection.object.el);
-
-    if ((this.isGrabbing || this.isInteractable) && !this.wasIntersecting) {
-      this.wasIntersecting = true;
-      this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered });
-    } else if (!this.isGrabbing && !this.isInteractable && this.wasIntersecting) {
-      this.wasIntersecting = false;
-      this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
-    }
-  },
-
-  _isInteractable: function(el) {
-    return (
-      el.className === "interactable" ||
-      (el.parentNode && el.parentNode != el.sceneEl && this._isInteractable(el.parentNode))
-    );
-  },
-
-  _handleMouseDown: function() {
-    if (this.isInteractable) {
-      const lookControls = this.data.camera.components["look-controls"];
-      if (lookControls) lookControls.pause();
-    }
-    this.data.cursor.emit("action_grab", {});
-  },
-
-  _handleMouseMove: function(e) {
-    this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
-  },
-
-  _handleMouseUp: function() {
-    const lookControls = this.data.camera.components["look-controls"];
-    if (lookControls) lookControls.play();
-    this.data.cursor.emit("action_release", {});
-  },
-
-  _handleWheel: function(e) {
-    if (this.isGrabbing) {
-      switch (e.deltaMode) {
-        case e.DOM_DELTA_PIXEL:
-          this.currentDistanceMod += e.deltaY / 500;
-          break;
-        case e.DOM_DELTA_LINE:
-          this.currentDistanceMod += e.deltaY / 10;
-          break;
-        case e.DOM_DELTA_PAGE:
-          this.currentDistanceMod += e.deltaY / 2;
-          break;
-      }
-    }
-  },
-
-  _handleEnterVR: function() {
-    if (AFRAME.utils.device.checkHeadsetConnected() || AFRAME.utils.device.isMobile()) {
-      this._disable();
-    }
-  },
-
-  _handleExitVR: function() {
-    this._enable();
-  },
-
-  _enable: function() {
-    this.enabled = true;
-    this.data.cursor.setAttribute("visible", true);
-    this.el.setAttribute("raycaster", { enabled: true });
-  },
-
-  _disable: function() {
-    this.enabled = false;
-    this.data.cursor.setAttribute("visible", false);
-    this.el.setAttribute("raycaster", { enabled: false });
-  }
-});
diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js
index bb6762a200eeacf53c29179f95554c7dc8fdab57..9d36c6583f01909e0db08f93450c78cd78c3c3aa 100644
--- a/src/components/super-spawner.js
+++ b/src/components/super-spawner.js
@@ -2,7 +2,8 @@ AFRAME.registerComponent("super-spawner", {
   schema: {
     template: { default: "" },
     useCustomSpawnPosition: { default: false },
-    spawnPosition: { type: "vec3" }
+    spawnPosition: { type: "vec3" },
+    events: { default: ["cursor-grab", "action_grab"] }
   },
 
   init: function() {
@@ -36,7 +37,6 @@ AFRAME.registerComponent("super-spawner", {
 
     const componentinInitializedListener = this._handleComponentInitialzed.bind(this, entity);
     const bodyLoadedListener = this._handleBodyLoaded.bind(this, entity);
-
     this.entities.set(entity, {
       hand: hand,
       componentInitialized: false,
@@ -68,8 +68,9 @@ AFRAME.registerComponent("super-spawner", {
   _emitEvents: function(entity) {
     const data = this.entities.get(entity);
     if (data.componentInitialized && data.bodyLoaded) {
-      data.hand.emit("action_grab", { targetEntity: entity });
-      entity.emit("grab-start", { hand: data.hand });
+      for (let i = 0; i < this.data.events.length; i++) {
+        data.hand.emit(this.data.events[i], { targetEntity: entity });
+      }
 
       entity.removeEventListener("componentinitialized", data.componentinInitializedListener);
       entity.removeEventListener("body-loaded", data.bodyLoadedListener);
diff --git a/src/hub.html b/src/hub.html
index 2ca2c34ce04b8b9f5cb03aef765140094687f7a8..6b759eb1ab65fb7bef6b43c2b3c4f261a8a50576 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -24,8 +24,6 @@
         physics
         mute-mic="eventSrc: a-scene; toggleEvents: action_mute"
         personal-space-bubble="debug: false;"
-
-        app-mode-input-mappings="modes: default, hud; actionSets: default, hud;"
         >
 
         <a-assets>
@@ -118,12 +116,12 @@
             </template>
 
             <a-mixin id="super-hands"
-                super-hands="colliderEvent: collisions; colliderEventProperty: els;
+                super-hands="
+                    colliderEvent: collisions; colliderEventProperty: els;
                     colliderEndEvent: collisions; colliderEndEventProperty: clearedEls;
                     grabStartButtons: action_grab; grabEndButtons: action_release;
                     stretchStartButtons: action_grab; stretchEndButtons: action_release;
-                    dragDropStartButtons: action_grab; dragDropEndButtons: action_release;
-                    "
+                    dragDropStartButtons: action_grab; dragDropEndButtons: action_release;"
                 collision-filter="collisionForces: false"
                 physics-collider
             ></a-mixin>
@@ -142,19 +140,33 @@
         ></a-entity>
 
         <a-entity
-            id="super-cursor"
-            super-cursor="cursor: #3d-cursor; camera: #player-camera;"
-            raycaster="objects: .collidable, .interactable; far: 10;"
-        >
-            <a-sphere
-                id="3d-cursor"
-                radius="0.02"
-                static-body="shape: sphere;"
-                mixin="super-hands"
-                segments-height="9"
-                segments-width="9"
-            ></a-sphere>  
-        </a-entity>
+            id="cursor-controller"
+            cursor-controller="
+                cursor: #cursor; 
+                camera: #player-camera; 
+                playerRig: #player-rig;
+                physicalHandSelector: #right-super-hand;
+                gazeTeleportControls: #gaze-teleport;"
+            raycaster="objects: .collidable, .interactable, .ui; far: 3;"
+            line="visible: false; color: white; opacity: 0.2;"
+        ></a-entity>
+
+        <a-sphere
+            id="cursor"
+            material="depthTest: false; opacity:0.9;"
+            radius="0.02"
+            static-body="shape: sphere;"
+            collision-filter="collisionForces: false"
+            super-hands="
+                colliderEvent: raycaster-intersection; colliderEventProperty: els;
+                colliderEndEvent: raycaster-intersection-cleared; colliderEndEventProperty: clearedEls;
+                grabStartButtons: cursor-grab; grabEndButtons: cursor-release;
+                stretchStartButtons: cursor-grab; stretchEndButtons: cursor-release;
+                dragDropStartButtons: cursor-grab; dragDropEndButtons: cursor-release;"
+            segments-height="9"
+            segments-width="9"
+            event-repeater="events: raycaster-intersection, raycaster-intersection-cleared; eventSource: #cursor-controller"
+        ></a-sphere> 
 
         <!-- Player Rig -->
         <a-entity
@@ -164,13 +176,11 @@
             wasd-to-analog2d
             character-controller="pivot: #player-camera"
             ik-root
-            app-mode-toggle-playing__character-controller="mode: hud; invert: true;"
-            app-mode-toggle-playing__wasd-to-analog2d="mode: hud; invert: true;"
             player-info
         >
-
             <a-entity
                 id="player-hud"
+                class="ui"
                 hud-controller="head: #player-camera;"
                 vr-mode-toggle-visibility
                 vr-mode-toggle-playing__hud-controller
@@ -189,7 +199,21 @@
                 camera
                 position="0 1.6 0"
                 personal-space-bubble="radius: 0.4"
-            ></a-entity>
+            >
+                <a-entity
+                    id="gaze-teleport"
+                    position = "0.15 0 0"
+                    teleport-controls="
+                    cameraRig: #player-rig; 
+                    teleportOrigin: #player-camera; 
+                    button: cursor-teleport_; 
+                    collisionEntities: [nav-mesh];
+                    drawIncrementally: true;
+                    incrementalDrawMs: 600;
+                    hitOpacity: 0.3;
+                    missOpacity: 0.2;"
+                ></a-entity>
+            </a-entity>
 
             <a-entity
                 id="player-left-controller"
@@ -197,15 +221,14 @@
                 hand-controls2="left"
                 tracked-controls
                 teleport-controls="
+                    cameraRig: #player-rig; 
+                    teleportOrigin: #player-camera; 
+                    button: cursor-teleport_; 
+                    collisionEntities: [nav-mesh];
                     drawIncrementally: true;
                     incrementalDrawMs: 600;
                     hitOpacity: 0.3;
-                    missOpacity: 0.2;
-                    cameraRig: #player-rig;
-                    teleportOrigin: #player-camera;
-                    button: action_teleport_;
-                    collisionEntities: [nav-mesh];"
-                app-mode-toggle-playing__teleport-controls="mode: hud; invert: true;"
+                    missOpacity: 0.2;"
                 haptic-feedback
             ></a-entity>
 
@@ -215,22 +238,15 @@
                 hand-controls2="right"
                 tracked-controls
                 teleport-controls="
+                    cameraRig: #player-rig; 
+                    teleportOrigin: #player-camera; 
+                    button: cursor-teleport_; 
+                    collisionEntities: [nav-mesh];
                     drawIncrementally: true;
                     incrementalDrawMs: 600;
                     hitOpacity: 0.3;
-                    missOpacity: 0.2;
-                    cameraRig: #player-rig;
-                    teleportOrigin: #player-camera;
-                    button: action_teleport_;
-                    collisionEntities: [nav-mesh];"
+                    missOpacity: 0.2;"
                 haptic-feedback
-                raycaster="objects:.hud; showLine: true; far: 2;"
-                cursor="fuse: false; downEvents: action_ui_select_down; upEvents: action_ui_select_up;"
-
-                app-mode-toggle-playing__teleport-controls="mode: hud; invert: true;"
-                app-mode-toggle-playing__raycaster="mode: hud;"
-                app-mode-toggle-playing__cursor="mode: hud;"
-                app-mode-toggle-attribute__line="mode: hud; property: visible;"
             ></a-entity>
 
             <a-entity gltf-model-plus="inflate: true;" class="model">
@@ -258,7 +274,10 @@
                 <template data-selector=".LeftHand">
                     <a-entity bone-visibility>
                         <a-entity
-                            event-repeater="events: action_grab, action_release; eventSource: #player-left-controller"
+                            id="left-super-hand"
+                            event-repeater="
+                            events: action_grab, action_release, action_primary_down, action_primary_up; 
+                            eventSource: #player-left-controller"
                             static-body="shape: sphere; sphereRadius: 0.02"
                             mixin="super-hands"
                             position="0 0.05 0"
@@ -269,7 +288,10 @@
                 <template data-selector=".RightHand">
                     <a-entity bone-visibility>
                         <a-entity
-                            event-repeater="events: action_grab, action_release; eventSource: #player-right-controller"
+                            id="right-super-hand"
+                            event-repeater="
+                            events: action_grab, action_release, action_primary_down, action_primary_up; 
+                            eventSource: #player-right-controller"
                             static-body="shape: sphere; sphereRadius: 0.02"
                             mixin="super-hands"
                             position="0 -0.05 0"
@@ -292,7 +314,6 @@
             id="environment-root" 
             nav-mesh-helper
             static-body="shape: none;"
-            class="collidable"
         ></a-entity>
 
         <a-entity
diff --git a/src/hub.js b/src/hub.js
index 5a5dfc3d34e435437ae0793cd22eb65a1252667a..a412bb4d909087dfd627a994df1ec214f4444dd7 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -82,9 +82,10 @@ import "super-hands";
 import "./components/super-networked-interactable";
 import "./components/networked-counter";
 import "./components/super-spawner";
-import "./components/super-cursor";
 import "./components/event-repeater";
 
+import "./components/cursor-controller";
+
 import "./components/nav-mesh-helper";
 
 import registerNetworkSchemas from "./network-schemas";
diff --git a/src/input-mappings.js b/src/input-mappings.js
index 8196d0119579623d9f7703bdae9e17bd724150d2..30345c9358003890ce1c0d1ed409bf4321ff0afd 100644
--- a/src/input-mappings.js
+++ b/src/input-mappings.js
@@ -40,18 +40,18 @@ const config = {
         "trackpad.pressedmove": { left: "move" },
         trackpad_dpad4_pressed_west_down: { right: "snap_rotate_left" },
         trackpad_dpad4_pressed_east_down: { right: "snap_rotate_right" },
-        trackpad_dpad4_pressed_center_down: { right: "action_teleport_down" },
-        trackpad_dpad4_pressed_north_down: { right: "action_teleport_down" },
-        trackpad_dpad4_pressed_south_down: { right: "action_teleport_down" },
-        trackpadup: { right: "action_teleport_up" },
+        trackpad_dpad4_pressed_center_down: { right: "action_primary_down" },
+        trackpad_dpad4_pressed_north_down: { right: "action_primary_down" },
+        trackpad_dpad4_pressed_south_down: { right: "action_primary_down" },
+        trackpadup: { right: "action_primary_up" },
         menudown: "thumb_down",
         menuup: "thumb_up",
         gripdown: ["action_grab", "middle_ring_pinky_down"],
         gripup: ["action_release", "middle_ring_pinky_up"],
         trackpadtouchstart: "thumb_down",
         trackpadtouchend: "thumb_up",
-        triggerdown: ["action_grab", "index_down"],
-        triggerup: ["action_release", "index_up"]
+        triggerdown: ["action_primary_down", "action_grab", "index_down"],
+        triggerup: ["action_primary_up", "action_release", "index_up"]
       },
       "oculus-touch-controls": {
         joystick_dpad4_west: {
@@ -74,27 +74,29 @@ const config = {
         surfacetouchend: "thumb_up",
         thumbsticktouchstart: "thumb_down",
         thumbsticktouchend: "thumb_up",
-        triggerdown: "index_down",
-        triggerup: "index_up",
+        triggerdown: ["action_primary_down", "action_grab", "index_down"],
+        triggerup: ["action_primary_up", "action_release", "index_up"],
         "axismove.reverseY": { left: "move" },
-        abuttondown: "action_teleport_down",
-        abuttonup: "action_teleport_up"
+        abuttondown: "action_primary_down",
+        abuttonup: "action_primary_up"
       },
       "daydream-controls": {
         trackpad_dpad4_pressed_west_down: "snap_rotate_left",
         trackpad_dpad4_pressed_east_down: "snap_rotate_right",
-        trackpad_dpad4_pressed_center_down: "action_teleport_down",
-        trackpad_dpad4_pressed_north_down: "action_teleport_down",
-        trackpad_dpad4_pressed_south_down: "action_teleport_down",
-        trackpadup: "action_teleport_up"
+        trackpad_dpad4_pressed_center_down: ["action_primary_down"],
+        trackpad_dpad4_pressed_north_down: ["action_primary_down"],
+        trackpad_dpad4_pressed_south_down: ["action_primary_down"],
+        trackpadup: ["action_primary_up"]
       },
       "gearvr-controls": {
         trackpad_dpad4_pressed_west_down: "snap_rotate_left",
         trackpad_dpad4_pressed_east_down: "snap_rotate_right",
-        trackpad_dpad4_pressed_center_down: "action_teleport_down",
-        trackpad_dpad4_pressed_north_down: "action_teleport_down",
-        trackpad_dpad4_pressed_south_down: "action_teleport_down",
-        trackpadup: "action_teleport_up"
+        trackpad_dpad4_pressed_center_down: ["action_primary_down"],
+        trackpad_dpad4_pressed_north_down: ["action_primary_down"],
+        trackpad_dpad4_pressed_south_down: ["action_primary_down"],
+        trackpadup: ["action_primary_up"],
+        triggerdown: ["action_primary_down"],
+        triggerup: ["action_primary_up"]
       },
       keyboard: {
         m_press: "action_mute",
diff --git a/yarn.lock b/yarn.lock
index 01d377b1e4441b058dc29d7eb865da622de5781a..96e3c1b583d7d84ba0eef1f880d865217529938b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -168,7 +168,7 @@ aframe-extras@^4.0.0:
 
 "aframe-input-mapping-component@https://github.com/johnshaughnessy/aframe-input-mapping-component#feature/map-to-array":
   version "0.1.2"
-  resolved "https://github.com/johnshaughnessy/aframe-input-mapping-component#4c7e493ad6c4a25eef27d32551c94d8b78541191"
+  resolved "https://github.com/johnshaughnessy/aframe-input-mapping-component#33d7ad4c82a5e2b74defca39c7fa5ef15aab493e"
 
 "aframe-physics-extras@https://github.com/infinitelee/aframe-physics-extras#fix/physics-collider-crash":
   version "0.1.2"
@@ -7745,7 +7745,7 @@ subarg@^1.0.0:
 
 "super-hands@https://github.com/infinitelee/aframe-super-hands-component#mr-social-client/master":
   version "2.1.0"
-  resolved "https://github.com/infinitelee/aframe-super-hands-component#0df9e947d652d9b8918e9bfd4db39169ed610ce2"
+  resolved "https://github.com/infinitelee/aframe-super-hands-component#e4d91f4b9bc91a57f35aa9d43f3e797ebfc5477d"
 
 supports-color@1.3.1:
   version "1.3.1"