diff --git a/src/App.js b/src/App.js
index c542f439f903e0a4426d087cb33d69852b25944d..b814e080d4e1df7ae5f6dbe7649d2548211f4c42 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,7 +1,10 @@
+import Store from "./storage/store";
+
 export class App {
   constructor() {
     this.scene = null;
     this.quality = "low";
+    this.store = new Store();
   }
 
   setQuality(quality) {
diff --git a/src/assets/stylesheets/2d-hud.css b/src/assets/stylesheets/2d-hud.css
index a50436114181d18997248dce77a3cc1d9500363f..d360067e0a24f93ce897d4c2549953d826af0a12 100644
--- a/src/assets/stylesheets/2d-hud.css
+++ b/src/assets/stylesheets/2d-hud.css
@@ -39,6 +39,13 @@
   align-items: center;
   justify-content: center;
   z-index: 10;
+  border-radius: 50%;
+  border: 2px solid white;
+  cursor: pointer;
+}
+
+:local(.modeButton.frozen) {
+  border-color: red;
 }
 
 :local(.panel) {
@@ -65,7 +72,6 @@
   background-size: 100%;
   background-image: url(../hud/avatar.png);
 }
-
 :local(.mic) {
   display: flex;
   width: 32px;
diff --git a/src/components/freeze-controller.js b/src/components/freeze-controller.js
new file mode 100644
index 0000000000000000000000000000000000000000..77e66f91f167d0f01ea4acac406c6f32eac83039
--- /dev/null
+++ b/src/components/freeze-controller.js
@@ -0,0 +1,27 @@
+AFRAME.registerComponent("freeze-controller", {
+  schema: {
+    toggleEvent: { type: "string" }
+  },
+
+  init: function() {
+    this.onToggle = this.onToggle.bind(this);
+  },
+
+  play: function() {
+    this.el.addEventListener(this.data.toggleEvent, this.onToggle);
+  },
+
+  pause: function() {
+    this.el.removeEventListener(this.data.toggleEvent, this.onToggle);
+  },
+
+  onToggle: function() {
+    window.APP.store.update({ profile: { has_found_freeze: true } });
+    NAF.connection.adapter.toggleFreeze();
+    if (NAF.connection.adapter.frozen) {
+      this.el.addState("frozen");
+    } else {
+      this.el.removeState("frozen");
+    }
+  }
+});
diff --git a/src/components/hud-controller.js b/src/components/hud-controller.js
index 4ee274f34ae9daa6f24c8e414eee824b845c49db..d2439b3765d0272ae5b113fa174b1738ab213c2b 100644
--- a/src/components/hud-controller.js
+++ b/src/components/hud-controller.js
@@ -15,7 +15,8 @@ AFRAME.registerComponent("hud-controller", {
     offset: { default: 0.7 }, // distance from hud above head,
     lookCutoff: { default: 20 }, // angle at which the hud should be "on",
     animRange: { default: 30 }, // degrees over which to animate the hud into view
-    yawCutoff: { default: 50 } // yaw degrees at wich the hud should reoirent even if the user is looking up
+    yawCutoff: { default: 50 }, // yaw degrees at wich the hud should reoirent even if the user is looking up
+    showTip: { type: "bool" }
   },
   init() {
     this.isYLocked = false;
@@ -33,14 +34,34 @@ AFRAME.registerComponent("hud-controller", {
     const head = this.data.head.object3D;
     const sceneEl = this.el.sceneEl;
 
-    const { offset, lookCutoff, animRange, yawCutoff } = this.data;
+    const { offset, lookCutoff, animRange, yawCutoff, showTip } = this.data;
 
     const pitch = head.rotation.x * THREE.Math.RAD2DEG;
     const yawDif = deltaAngle(head.rotation.y, hud.rotation.y) * THREE.Math.RAD2DEG;
 
-    // Reorient the hud only if the user is looking away from the hud, for right now this arbitrarily means the hud is 1/3 way animated away
+    // animate the hud into place over animRange degrees as the user aproaches the lookCutoff angle
+    let t = 1 - THREE.Math.clamp(lookCutoff - pitch, 0, animRange) / animRange;
+
+    // HUD is locked down while showing tooltip
+    if (showTip) {
+      t = 1;
+    }
+
+    // Once the HUD is in place it should stay in place until you look sufficiently far down
+    if (t === 1) {
+      this.lockedHeadPositionY = head.position.y;
+      this.hudLocked = true;
+    } else if (this.hudLocked && pitch < lookCutoff - animRange / 2) {
+      this.hudLocked = false;
+    }
+
+    if (this.hudLocked) {
+      t = 1;
+    }
+
+    // Reorient the hud only if the user is looking away from the hud, for right now this arbitrarily means the hud is 1/2 way animated away
     // TODO: come up with better huristics for this that maybe account for the user turning away from the hud "too far", also animate the position so that it doesnt just snap.
-    if (yawDif >= yawCutoff || pitch < lookCutoff - animRange / 3) {
+    if (yawDif >= yawCutoff || pitch < lookCutoff - animRange / 2) {
       const lookDir = new THREE.Vector3(0, 0, -1);
       lookDir.applyQuaternion(head.quaternion);
       lookDir.add(head.position);
@@ -48,18 +69,6 @@ AFRAME.registerComponent("hud-controller", {
       hud.position.z = lookDir.z;
       hud.setRotationFromEuler(new THREE.Euler(0, head.rotation.y, 0));
     }
-
-    // animate the hud into place over animRange degrees as the user aproaches the lookCutoff angle
-    const t = 1 - THREE.Math.clamp(lookCutoff - pitch, 0, animRange) / animRange;
-
-    // Lock the hud in place relative to a known head position so it doesn't bob up and down
-    // with the user's head
-    if (!this.isYLocked && t === 1) {
-      this.lockedHeadPositionY = head.position.y;
-    }
-    const EPSILON = 0.001;
-    this.isYLocked = t > 1 - EPSILON;
-
     hud.position.y = (this.isYLocked ? this.lockedHeadPositionY : head.position.y) + offset + (1 - t) * offset;
     hud.rotation.x = (1 - t) * THREE.Math.DEG2RAD * 90;
 
diff --git a/src/hub.html b/src/hub.html
index edb7395d4641f10c27994b3399dc580e72f6952b..973461f270e5b2f412421d8f3ba5ed06b4dc3328 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -22,6 +22,7 @@
         networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;"
         physics
         mute-mic="eventSrc: a-scene; toggleEvents: action_mute"
+        freeze-controller="toggleEvent: action_freeze"
         personal-space-bubble="debug: false;"
 
         app-mode-input-mappings="modes: default, hud; actionSets: default, hud;"
diff --git a/src/hub.js b/src/hub.js
index 75111f9c6b4abdee9ef8ecb771d195e02f17be38..34d2812823ff6b2aa3926dbf5f20afa6c47da577 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -49,6 +49,7 @@ import "./components/hand-poses";
 import "./components/gltf-model-plus";
 import "./components/gltf-bundle";
 import "./components/hud-controller";
+import "./components/freeze-controller";
 
 import ReactDOM from "react-dom";
 import React from "react";
@@ -63,6 +64,7 @@ import "./gltf-component-mappings";
 import { App } from "./App";
 
 window.APP = new App();
+const store = window.APP.store;
 
 const qs = queryString.parse(location.search);
 const isMobile = AFRAME.utils.device.isMobile();
@@ -88,7 +90,6 @@ import "./components/nav-mesh-helper";
 import registerNetworkSchemas from "./network-schemas";
 import { inGameActions, config as inputConfig } from "./input-mappings";
 import registerTelemetry from "./telemetry";
-import Store from "./storage/store";
 
 import { generateDefaultProfile, generateRandomName } from "./utils/identity.js";
 import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js";
@@ -108,7 +109,6 @@ AFRAME.registerInputActivator("pressedmove", PressedMove);
 AFRAME.registerInputActivator("reverseY", ReverseY);
 AFRAME.registerInputMappings(inputConfig, true);
 
-const store = new Store();
 const concurrentLoadDetector = new ConcurrentLoadDetector();
 const hubChannel = new HubChannel(store);
 
@@ -135,6 +135,8 @@ function applyProfileFromStore(playerRig) {
     displayName,
     avatarSrc: "#" + (store.state.profile.avatar_id || "botdefault")
   });
+  const hudController = playerRig.querySelector("[hud-controller]");
+  hudController.setAttribute("hud-controller", { showTip: !store.state.profile.has_found_freeze });
   document.querySelector("a-scene").emit("username-changed", { username: displayName });
 }
 
diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js
index 9679bb9c090af49966c6f5cbf03072201c860f00..8d35fada2457db7ef74728e9547b4123223c3e00 100644
--- a/src/react-components/2d-hud.js
+++ b/src/react-components/2d-hud.js
@@ -4,12 +4,12 @@ import cx from "classnames";
 
 import styles from "../assets/stylesheets/2d-hud.css";
 
-const TwoDHUD = ({ muted, onToggleMute }) => (
+const TwoDHUD = ({ muted, frozen, onToggleMute, onToggleFreeze }) => (
   <div className={styles.container}>
     <div className={cx("ui-interactive", styles.panel, styles.left)}>
       <div className={cx(styles.mic, { [styles.muted]: muted })} onClick={onToggleMute} />
     </div>
-    <div className={cx("ui-interactive", styles.modeButton)}>
+    <div className={cx("ui-interactive", styles.modeButton, { [styles.frozen]: frozen })} onClick={onToggleFreeze}>
       <div className={styles.avatar} />
     </div>
     <div className={cx("ui-interactive", styles.panel, styles.right)}>
@@ -20,7 +20,9 @@ const TwoDHUD = ({ muted, onToggleMute }) => (
 
 TwoDHUD.propTypes = {
   muted: PropTypes.bool,
-  onToggleMute: PropTypes.func
+  frozen: PropTypes.bool,
+  onToggleMute: PropTypes.func,
+  onToggleFreeze: PropTypes.func
 };
 
 export default TwoDHUD;
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index 3e22208dacc6219b59cc9276562c71d43f129f03..bbf19e97eadc58dd0e5367bbd2981130d0b671bc 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -90,6 +90,9 @@ class UIRoot extends Component {
     autoExitTimerInterval: null,
     secondsRemainingBeforeAutoExit: Infinity,
 
+    muted: false,
+    frozen: false,
+
     exited: false,
 
     showProfileEntry: false
@@ -121,9 +124,9 @@ class UIRoot extends Component {
 
   // TODO: mute state should probably actually just live in react land
   onAframeStateChanged = e => {
-    if (e.detail !== "muted") return;
+    if (!(e.detail === "muted" || e.detail === "frozen")) return;
     this.setState({
-      muted: this.props.scene.is("muted")
+      [e.detail]: this.props.scene.is(e.detail)
     });
   };
 
@@ -131,6 +134,10 @@ class UIRoot extends Component {
     this.props.scene.emit("action_mute");
   };
 
+  toggleFreeze = () => {
+    this.props.scene.emit("action_freeze");
+  };
+
   handleForcedVREntryType = () => {
     if (!this.props.forcedVREntryType) return;
 
@@ -758,7 +765,12 @@ class UIRoot extends Component {
             )}
           </div>
           {this.state.entryStep === ENTRY_STEPS.finished ? (
-            <TwoDHUD muted={this.state.muted} onToggleMute={this.toggleMute} />
+            <TwoDHUD
+              muted={this.state.muted}
+              frozen={this.state.frozen}
+              onToggleMute={this.toggleMute}
+              onToggleFreeze={this.toggleFreeze}
+            />
           ) : null}
         </div>
       </IntlProvider>
diff --git a/src/storage/store.js b/src/storage/store.js
index 4351ebeda99e9f9665fcbbf395858296cd2e56a4..b117e1849bf6a20cc28d770c7ef66112bb8888ba 100644
--- a/src/storage/store.js
+++ b/src/storage/store.js
@@ -19,6 +19,7 @@ export const SCHEMA = {
       properties: {
         has_agreed_to_terms: { type: "boolean" },
         has_changed_name: { type: "boolean" },
+        has_found_freeze: { type: "boolean" },
         display_name: { type: "string", pattern: "^[A-Za-z0-9-]{3,32}$" },
         avatar_id: { type: "string" }
       }
diff --git a/src/utils/identity.js b/src/utils/identity.js
index db78b027e3e851aa254532f264438e362062576c..4cce7fa3729e376be6d28d57eb3120715bceee8b 100644
--- a/src/utils/identity.js
+++ b/src/utils/identity.js
@@ -103,6 +103,7 @@ export function generateDefaultProfile() {
   return {
     has_agreed_to_terms: false,
     has_changed_name: false,
+    has_found_freeze: false,
     avatar_id: selectRandom(avatarIds)
   };
 }