diff --git a/package.json b/package.json
index 7309a9d77dad1954fa1e49254de2fc4957d2bbdc..190ff318264873ec03c11b0744566b2ac1935502 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,6 @@
   },
   "dependencies": {
     "aframe-billboard-component": "^1.0.0",
-    "aframe-extras": "^3.12.4",
     "aframe-input-mapping-component": "https://github.com/johnshaughnessy/aframe-input-mapping-component#feature/map-to-array",
     "aframe-teleport-controls": "https://github.com/netpro2k/aframe-teleport-controls#feature/teleport-origin",
     "aframe-xr": "github:brianpeiris/aframe-xr#3162aed",
diff --git a/src/assets/avatars/BotDefault_Avatar.glb b/src/assets/avatars/BotDefault_Avatar.glb
index ccb77bf21362f3a5b2058f59eafd56fa17a55ec4..0784e7354e0fe2e35b3df8329c5587616203b7c3 100644
Binary files a/src/assets/avatars/BotDefault_Avatar.glb and b/src/assets/avatars/BotDefault_Avatar.glb differ
diff --git a/src/assets/avatars/BotDefault_Avatar_Unlit.glb b/src/assets/avatars/BotDefault_Avatar_Unlit.glb
index ba33589d4c42fe6bf9756d5164f331e9830dab91..850f7552c4183553779dfe0776fb92f042b7f8ca 100644
Binary files a/src/assets/avatars/BotDefault_Avatar_Unlit.glb and b/src/assets/avatars/BotDefault_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/Bot_SingleSkin_Rigged.glb b/src/assets/avatars/Bot_SingleSkin_Rigged.glb
deleted file mode 100755
index 468db3fd21422c94a19c67fc3fd5e0d4e7b8e447..0000000000000000000000000000000000000000
Binary files a/src/assets/avatars/Bot_SingleSkin_Rigged.glb and /dev/null differ
diff --git a/src/components/animated-robot-hands.js b/src/components/animated-robot-hands.js
index ef7b429d177864cc4d78a907f333e5e38411d7f9..1b26402848a8cb641560d76307708c4d77d53352 100644
--- a/src/components/animated-robot-hands.js
+++ b/src/components/animated-robot-hands.js
@@ -13,24 +13,25 @@ const POSES = {
 // TODO: When we have analog values of index-finger triggers or middle-finger grips,
 //       it would be nice to animate the hands proportionally to those analog values.
 AFRAME.registerComponent("animated-robot-hands", {
+  dependencies: ["animation-mixer"],
   schema: {
     leftHand: { type: "selector", default: "#player-left-controller" },
     rightHand: { type: "selector", default: "#player-right-controller" }
   },
 
   init: function() {
-    window.hands = this;
     this.playAnimation = this.playAnimation.bind(this);
 
-    // Get the three.js object in the scene graph that has the animation data
-    const root = this.el.querySelector("a-gltf-entity .RootScene").object3D.children[0];
-    this.mixer = new THREE.AnimationMixer(root);
-    this.root = root;
+    this.mixer = this.el.components["animation-mixer"].mixer;
 
-    // Set hands to open pose because the bind pose is funky due
+    const object3DMap = this.el.object3DMap;
+    const rootObj = object3DMap.mesh || object3DMap.scene;
+    this.clipActionObject = rootObj.parent;
+
+    // Set hands to open pose because the bind pose is funky dues
     // to the workaround for FBX2glTF animations.
-    this.openL = this.mixer.clipAction(POSES.open + "_L", root.parent);
-    this.openR = this.mixer.clipAction(POSES.open + "_R", root.parent);
+    this.openL = this.mixer.clipAction(POSES.open + "_L", this.clipActionObject);
+    this.openR = this.mixer.clipAction(POSES.open + "_R", this.clipActionObject);
     this.openL.play();
     this.openR.play();
   },
@@ -45,10 +46,6 @@ AFRAME.registerComponent("animated-robot-hands", {
     this.data.rightHand.removeEventListener("hand-pose", this.playAnimation);
   },
 
-  tick: function(t, dt) {
-    this.mixer.update(dt / 1000);
-  },
-
   // Animate from pose to pose.
   // TODO: Transition from current pose (which may be BETWEEN two other poses)
   //       to the target pose, rather than stopping previous actions altogether.
@@ -81,8 +78,8 @@ AFRAME.registerComponent("animated-robot-hands", {
     //    console.log(
     //      `Animating ${isLeft ? "left" : "right"} hand from ${prevPose} to ${currPose} over ${duration} seconds.`
     //    );
-    const from = mixer.clipAction(prevPose, this.root.parent);
-    const to = mixer.clipAction(currPose, this.root.parent);
+    const from = mixer.clipAction(prevPose, this.clipActionObject);
+    const to = mixer.clipAction(currPose, this.clipActionObject);
     from.fadeOut(duration);
     to.fadeIn(duration);
     to.play();
diff --git a/src/components/animation-mixer.js b/src/components/animation-mixer.js
new file mode 100644
index 0000000000000000000000000000000000000000..791854f1eab2b6f30f20f094b9fb9a35c3ecb9f2
--- /dev/null
+++ b/src/components/animation-mixer.js
@@ -0,0 +1,36 @@
+AFRAME.registerComponent("animation-mixer", {
+  init() {
+    this.mixer = null;
+
+    const object3DMap = this.el.object3DMap;
+    const rootObject3D = object3DMap.mesh || object3DMap.scene;
+
+    if (rootObject3D) {
+      this.setAnimationMixer(rootObject3D);
+    } else {
+      this.onModelLoaded = this.onModelLoaded.bind(this);
+      this.el.addEventListener("model-loaded", this.onModelLoaded);
+    }
+  },
+
+  onModelLoaded(event) {
+    const sceneObject3D = event.detail.model;
+    this.setAnimationMixer(sceneObject3D);
+
+    this.el.removeEventListener(this.onModelLoaded);
+  },
+
+  setAnimationMixer(rootObject3D) {
+    this.mixer = new THREE.AnimationMixer(rootObject3D);
+  },
+
+  tick: function(t, dt) {
+    if (this.mixer) {
+      this.mixer.update(dt / 1000);
+    }
+  },
+
+  destroy() {
+    this.el.removeEventListener(this.onModelLoaded);
+  }
+});
diff --git a/src/components/loop-animation.js b/src/components/loop-animation.js
new file mode 100644
index 0000000000000000000000000000000000000000..09a9e9dafabf8c05c7f13a02a9ecb820018ef573
--- /dev/null
+++ b/src/components/loop-animation.js
@@ -0,0 +1,58 @@
+AFRAME.registerComponent("loop-animation", {
+  dependencies: ["animation-mixer"],
+  schema: {
+    clip: { type: "string", required: true }
+  },
+  init() {
+    const object3DMap = this.el.object3DMap;
+    this.model = object3DMap.mesh || object3DMap.scene;
+
+    if (this.model) {
+      this.mixer = this.el.components["animation-mixer"].mixer;
+    } else {
+      this.onModelLoaded = this.onModelLoaded.bind(this);
+      this.el.addEventListener("model-loaded", this.onModelLoaded);
+    }
+  },
+
+  onModelLoaded(event) {
+    const animationMixerComponent = this.el.components["animation-mixer"];
+    this.model = event.detail.model;
+    this.mixer = animationMixerComponent.mixer;
+
+    this.updateClipState(true);
+
+    this.el.removeEventListener(this.onModelLoaded);
+  },
+
+  update(oldData) {
+    if (oldData.clip !== this.data.clip && this.model) {
+      this.updateClipState(true);
+    }
+  },
+
+  updateClipState(play) {
+    const model = this.model;
+    const clipName = this.data.clip;
+
+    for (const clip of this.model.animations) {
+      if (clip.name === clipName) {
+        const action = this.mixer.clipAction(clip, model.parent);
+
+        if (play) {
+          action.enabled = true;
+          action.setLoop(THREE.LoopRepeat, Infinity).play();
+        } else {
+          action.stop();
+        }
+
+        break;
+      }
+    }
+  },
+
+  destroy() {
+    this.updateClipState(false);
+    this.el.removeEventListener(this.onModelLoaded);
+  }
+});
diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js
index e73692a8ff9270985f3b533ff8bd1f8fdc0cb835..e4cf086733ad1e0dc547abafe3fb37ed025a5608 100644
--- a/src/gltf-component-mappings.js
+++ b/src/gltf-component-mappings.js
@@ -1,3 +1,4 @@
 import "./elements/a-gltf-entity";
 
 AFRAME.AGLTFEntity.registerComponent("scale-audio-feedback", "scale-audio-feedback");
+AFRAME.AGLTFEntity.registerComponent("loop-animation", "loop-animation");
diff --git a/src/room.html b/src/room.html
index bf25b9b22a87d6c4702546bba5abac4e179f2709..96e34430192a2c41611f27eaf4cf1a2a5799d4d4 100644
--- a/src/room.html
+++ b/src/room.html
@@ -58,6 +58,10 @@
                     <a-entity class="right-controller"></a-entity>
 
                     <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller >
+                        <template data-selector=".RootScene">
+                            <a-entity animation-mixer ></a-entity>
+                        </template>
+
                         <template data-selector=".Neck">
                              <a-entity>
                                  <a-entity
@@ -100,7 +104,6 @@
             wasd-to-analog2d
             character-controller="pivot: #player-camera"
             ik-root
-            animated-robot-hands
         >
             <a-entity
                 id="player-camera"
@@ -129,7 +132,11 @@
                 haptic-feedback
             ></a-entity>
 
-            <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller>
+            <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller >
+                <template data-selector=".RootScene">
+                    <a-entity animation-mixer animated-robot-hands ></a-entity>
+                </template>
+
                 <template data-selector=".Neck">
                     <a-entity>
                         <a-entity class="nametag" visible="false" text ></a-entity>
diff --git a/src/room.js b/src/room.js
index 5af49f9b95507ad437d018af44d85251b425ed34..9e5f4aed820d53f372bdb842ddaa48be8fd71657 100644
--- a/src/room.js
+++ b/src/room.js
@@ -13,9 +13,6 @@ import "aframe-input-mapping-component";
 import "aframe-billboard-component";
 import "webrtc-adapter";
 
-import animationMixer from "aframe-extras/src/loaders/animation-mixer";
-AFRAME.registerComponent("animation-mixer", animationMixer);
-
 import { vive_trackpad_dpad4 } from "./behaviours/vive-trackpad-dpad4";
 import { oculus_touch_joystick_dpad4 } from "./behaviours/oculus-touch-joystick-dpad4";
 import { PressedMove } from "./activators/pressedmove";
@@ -40,6 +37,8 @@ import "./components/layers";
 import "./components/spawn-controller";
 import "./components/animated-robot-hands";
 import "./components/hide-when-quality";
+import "./components/animation-mixer";
+import "./components/loop-animation";
 
 import "./systems/personal-space-bubble";
 
diff --git a/yarn.lock b/yarn.lock
index 7a240ecf65a49d83a87c51db8a9056bb9af94b1a..7284e9761b90246d2c0f0981f68334d756cc9de6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -44,13 +44,6 @@ aframe-billboard-component@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/aframe-billboard-component/-/aframe-billboard-component-1.0.0.tgz#10ce2482729eef7386c5844d65917581a62d3adc"
 
-aframe-extras@^3.12.4:
-  version "3.13.1"
-  resolved "https://registry.yarnpkg.com/aframe-extras/-/aframe-extras-3.13.1.tgz#f8b6ef18c29e92538d05d94913640942a307c46c"
-  dependencies:
-    aframe-physics-system "^1.4.3"
-    three-pathfinding "^0.2.2"
-
 "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"
@@ -61,13 +54,6 @@ aframe-lerp-component@^1.1.0:
   dependencies:
     almost-equal "^1.1.0"
 
-aframe-physics-system@^1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/aframe-physics-system/-/aframe-physics-system-1.4.3.tgz#c6927e847081bfe546658314aa4c04958ef27934"
-  dependencies:
-    cannon "github:donmccurdy/cannon.js#v0.6.2-dev1"
-    three-to-cannon "^1.1.1"
-
 "aframe-teleport-controls@https://github.com/netpro2k/aframe-teleport-controls#feature/teleport-origin":
   version "0.3.0"
   resolved "https://github.com/netpro2k/aframe-teleport-controls#41fe311d3123503ba44761acce69d0f0634139cc"
@@ -1360,10 +1346,6 @@ caniuse-lite@^1.0.30000792:
   version "1.0.30000810"
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000810.tgz#47585fffce0e9f3593a6feea4673b945424351d9"
 
-"cannon@github:donmccurdy/cannon.js#v0.6.2-dev1":
-  version "0.6.2"
-  resolved "https://codeload.github.com/donmccurdy/cannon.js/tar.gz/022e8ba53fa83abf0ad8a0e4fd08623123838a17"
-
 caseless@~0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@@ -6443,14 +6425,6 @@ textextensions@2:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286"
 
-three-pathfinding@^0.2.2:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/three-pathfinding/-/three-pathfinding-0.2.3.tgz#469bb26fb6b331f536c9ec88fde78e9c9219f637"
-
-three-to-cannon@^1.1.1:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/three-to-cannon/-/three-to-cannon-1.2.0.tgz#92b9a756a270851aa98c3058c51ef15891507c01"
-
 through2@^2.0.0, through2@^2.0.1:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"