text: /**
    * Simple gesture controllers with some common gestures that emit
    * gesture events.
    *
    * Ported from github.com/EightMedia/hammer.js Gestures - thanks!
    */
  (function(ionic) {
  
    /**
     * ionic.Gestures
     * use this to create instances
     * @param   {HTMLElement}   element
     * @param   {Object}        options
     * @returns {ionic.Gestures.Instance}
     * @constructor
     */
    ionic.Gesture = function(element, options) {
      return new ionic.Gestures.Instance(element, options || {});
    };
  
    ionic.Gestures = {};
  
    // default settings
    ionic.Gestures.defaults = {
      // add css to the element to prevent the browser from doing
      // its native behavior. this doesnt prevent the scrolling,
      // but cancels the contextmenu, tap highlighting etc
      // set to false to disable this
      stop_browser_behavior: 'disable-user-behavior'
    };
  
    // detect touchevents
    ionic.Gestures.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled;
    ionic.Gestures.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  
    // dont use mouseevents on mobile devices
    ionic.Gestures.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i;
    ionic.Gestures.NO_MOUSEEVENTS = ionic.Gestures.HAS_TOUCHEVENTS && window.navigator.userAgent.match(ionic.Gestures.MOBILE_REGEX);
  
    // eventtypes per touchevent (start, move, end)
    // are filled by ionic.Gestures.event.determineEventTypes on setup
    ionic.Gestures.EVENT_TYPES = {};
  
    // direction defines
    ionic.Gestures.DIRECTION_DOWN = 'down';
    ionic.Gestures.DIRECTION_LEFT = 'left';
    ionic.Gestures.DIRECTION_UP = 'up';
    ionic.Gestures.DIRECTION_RIGHT = 'right';
  
    // pointer type
    ionic.Gestures.POINTER_MOUSE = 'mouse';
    ionic.Gestures.POINTER_TOUCH = 'touch';
    ionic.Gestures.POINTER_PEN = 'pen';
  
    // touch event defines
    ionic.Gestures.EVENT_START = 'start';
    ionic.Gestures.EVENT_MOVE = 'move';
    ionic.Gestures.EVENT_END = 'end';
  
    // hammer document where the base events are added at
    ionic.Gestures.DOCUMENT = window.document;
  
    // plugins namespace
    ionic.Gestures.plugins = {};
  
    // if the window events are set...
    ionic.Gestures.READY = false;
  
    /**
     * setup events to detect gestures on the document
     */
    function setup() {
      if(ionic.Gestures.READY) {
        return;
      }
  
      // find what eventtypes we add listeners to
      ionic.Gestures.event.determineEventTypes();
  
      // Register all gestures inside ionic.Gestures.gestures
      for(var name in ionic.Gestures.gestures) {
        if(ionic.Gestures.gestures.hasOwnProperty(name)) {
          ionic.Gestures.detection.register(ionic.Gestures.gestures[name]);
        }
      }
  
      // Add touch events on the document
      ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_MOVE, ionic.Gestures.detection.detect);
      ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_END, ionic.Gestures.detection.detect);
  
      // ionic.Gestures is ready...!
      ionic.Gestures.READY = true;
    }
  
    /**
     * create new hammer instance
     * all methods should return the instance itself, so it is chainable.
     * @param   {HTMLElement}       element
     * @param   {Object}            [options={}]
     * @returns {ionic.Gestures.Instance}
     * @name Gesture.Instance
     * @constructor
     */
    ionic.Gestures.Instance = function(element, options) {
      var self = this;
  
      // A null element was passed into the instance, which means
      // whatever lookup was done to find this element failed to find it
      // so we can't listen for events on it.
      if(element === null) {
        console.error('Null element passed to gesture (element does not exist). Not listening for gesture');
        return;
      }
  
      // setup ionic.GesturesJS window events and register all gestures
      // this also sets up the default options
      setup();
  
      this.element = element;
  
      // start/stop detection option
      this.enabled = true;
  
      // merge options
      this.options = ionic.Gestures.utils.extend(
          ionic.Gestures.utils.extend({}, ionic.Gestures.defaults),
          options || {});
  
      // add some css to the element to prevent the browser from doing its native behavoir
      if(this.options.stop_browser_behavior) {
        ionic.Gestures.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
      }
  
      // start detection on touchstart
      ionic.Gestures.event.onTouch(element, ionic.Gestures.EVENT_START, function(ev) {
        if(self.enabled) {
          ionic.Gestures.detection.startDetect(self, ev);
        }
      });
  
      // return instance
      return this;
    };
  
  
    ionic.Gestures.Instance.prototype = {
      /**
       * bind events to the instance
       * @param   {String}      gesture
       * @param   {Function}    handler
       * @returns {ionic.Gestures.Instance}
       */
      on: function onEvent(gesture, handler){
        var gestures = gesture.split(' ');
        for(var t=0; t<gestures.length; t++) {
          this.element.addEventListener(gestures[t], handler, false);
        }
        return this;
      },
  
  
      /**
       * unbind events to the instance
       * @param   {String}      gesture
       * @param   {Function}    handler
       * @returns {ionic.Gestures.Instance}
       */
      off: function offEvent(gesture, handler){
        var gestures = gesture.split(' ');
        for(var t=0; t<gestures.length; t++) {
          this.element.removeEventListener(gestures[t], handler, false);
        }
        return this;
      },
  
  
      /**
       * trigger gesture event
       * @param   {String}      gesture
       * @param   {Object}      eventData
       * @returns {ionic.Gestures.Instance}
       */
      trigger: function triggerEvent(gesture, eventData){
        // create DOM event
        var event = ionic.Gestures.DOCUMENT.createEvent('Event');
        event.initEvent(gesture, true, true);
        event.gesture = eventData;
  
        // trigger on the target if it is in the instance element,
        // this is for event delegation tricks
        var element = this.element;
        if(ionic.Gestures.utils.hasParent(eventData.target, element)) {
          element = eventData.target;
        }
  
        element.dispatchEvent(event);
        return this;
      },
  
  
      /**
       * enable of disable hammer.js detection
       * @param   {Boolean}   state
       * @returns {ionic.Gestures.Instance}
       */
      enable: function enable(state) {
        this.enabled = state;
        return this;
      }
    };
  
    /**
     * this holds the last move event,
     * used to fix empty touchend issue
     * see the onTouch event for an explanation
     * type {Object}
     */
    var last_move_event = null;
  
  
    /**
     * when the mouse is hold down, this is true
     * type {Boolean}
     */
    var enable_detect = false;
  
  
    /**
     * when touch events have been fired, this is true
     * type {Boolean}
     */
    var touch_triggered = false;
  
  
    ionic.Gestures.event = {
      /**
       * simple addEventListener
       * @param   {HTMLElement}   element
       * @param   {String}        type
       * @param   {Function}      handler
       */
      bindDom: function(element, type, handler) {
        var types = type.split(' ');
        for(var t=0; t<types.length; t++) {
          element.addEventListener(types[t], handler, false);
        }
      },
  
  
      /**
       * touch events with mouse fallback
       * @param   {HTMLElement}   element
       * @param   {String}        eventType        like ionic.Gestures.EVENT_MOVE
       * @param   {Function}      handler
       */
      onTouch: function onTouch(element, eventType, handler) {
        var self = this;
  
        this.bindDom(element, ionic.Gestures.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
          var sourceEventType = ev.type.toLowerCase();
  
          // onmouseup, but when touchend has been fired we do nothing.
          // this is for touchdevices which also fire a mouseup on touchend
          if(sourceEventType.match(/mouse/) && touch_triggered) {
            return;
          }
  
          // mousebutton must be down or a touch event
          else if( sourceEventType.match(/touch/) ||   // touch events are always on screen
            sourceEventType.match(/pointerdown/) || // pointerevents touch
            (sourceEventType.match(/mouse/) && ev.which === 1)   // mouse is pressed
            ){
              enable_detect = true;
            }
  
          // mouse isn't pressed
          else if(sourceEventType.match(/mouse/) && ev.which !== 1) {
            enable_detect = false;
          }
  
  
          // we are in a touch event, set the touch triggered bool to true,
          // this for the conflicts that may occur on ios and android
          if(sourceEventType.match(/touch|pointer/)) {
            touch_triggered = true;
          }
  
          // count the total touches on the screen
          var count_touches = 0;
  
          // when touch has been triggered in this detection session
          // and we are now handling a mouse event, we stop that to prevent conflicts
          if(enable_detect) {
            // update pointerevent
            if(ionic.Gestures.HAS_POINTEREVENTS && eventType != ionic.Gestures.EVENT_END) {
              count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
            }
            // touch
            else if(sourceEventType.match(/touch/)) {
              count_touches = ev.touches.length;
            }
            // mouse
            else if(!touch_triggered) {
              count_touches = sourceEventType.match(/up/) ? 0 : 1;
            }
  
            // if we are in a end event, but when we remove one touch and
            // we still have enough, set eventType to move
            if(count_touches > 0 && eventType == ionic.Gestures.EVENT_END) {
              eventType = ionic.Gestures.EVENT_MOVE;
            }
            // no touches, force the end event
            else if(!count_touches) {
              eventType = ionic.Gestures.EVENT_END;
            }
  
            // store the last move event
            if(count_touches || last_move_event === null) {
              last_move_event = ev;
            }
  
            // trigger the handler
            handler.call(ionic.Gestures.detection, self.collectEventData(element, eventType, self.getTouchList(last_move_event, eventType), ev));
  
            // remove pointerevent from list
            if(ionic.Gestures.HAS_POINTEREVENTS && eventType == ionic.Gestures.EVENT_END) {
              count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
            }
          }
  
          //debug(sourceEventType +" "+ eventType);
  
          // on the end we reset everything
          if(!count_touches) {
            last_move_event = null;
            enable_detect = false;
            touch_triggered = false;
            ionic.Gestures.PointerEvent.reset();
          }
        });
      },
  
  
      /**
       * we have different events for each device/browser
       * determine what we need and set them in the ionic.Gestures.EVENT_TYPES constant
       */
      determineEventTypes: function determineEventTypes() {
        // determine the eventtype we want to set
        var types;
  
        // pointerEvents magic
        if(ionic.Gestures.HAS_POINTEREVENTS) {
          types = ionic.Gestures.PointerEvent.getEvents();
        }
        // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
        else if(ionic.Gestures.NO_MOUSEEVENTS) {
          types = [
            'touchstart',
            'touchmove',
            'touchend touchcancel'];
        }
        // for non pointer events browsers and mixed browsers,
        // like chrome on windows8 touch laptop
        else {
          types = [
            'touchstart mousedown',
            'touchmove mousemove',
            'touchend touchcancel mouseup'];
        }
  
        ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_START]  = types[0];
        ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_MOVE]   = types[1];
        ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_END]    = types[2];
      },
  
  
      /**
       * create touchlist depending on the event
       * @param   {Object}    ev
       * @param   {String}    eventType   used by the fakemultitouch plugin
       */
      getTouchList: function getTouchList(ev/*, eventType*/) {
        // get the fake pointerEvent touchlist
        if(ionic.Gestures.HAS_POINTEREVENTS) {
          return ionic.Gestures.PointerEvent.getTouchList();
        }
        // get the touchlist
        else if(ev.touches) {
          return ev.touches;
        }
        // make fake touchlist from mouse position
        else {
          ev.identifier = 1;
          return [ev];
        }
      },
  
  
      /**
       * collect event data for ionic.Gestures js
       * @param   {HTMLElement}   element
       * @param   {String}        eventType        like ionic.Gestures.EVENT_MOVE
       * @param   {Object}        eventData
       */
      collectEventData: function collectEventData(element, eventType, touches, ev) {
  
        // find out pointerType
        var pointerType = ionic.Gestures.POINTER_TOUCH;
        if(ev.type.match(/mouse/) || ionic.Gestures.PointerEvent.matchType(ionic.Gestures.POINTER_MOUSE, ev)) {
          pointerType = ionic.Gestures.POINTER_MOUSE;
        }
  
        return {
          center      : ionic.Gestures.utils.getCenter(touches),
                      timeStamp   : new Date().getTime(),
                      target      : ev.target,
                      touches     : touches,
                      eventType   : eventType,
                      pointerType : pointerType,
                      srcEvent    : ev,
  
                      /**
                       * prevent the browser default actions
                       * mostly used to disable scrolling of the browser
                       */
                      preventDefault: function() {
                        if(this.srcEvent.preventManipulation) {
                          this.srcEvent.preventManipulation();
                        }
  
                        if(this.srcEvent.preventDefault) {
                          // this.srcEvent.preventDefault();
                        }
                      },
  
                      /**
                       * stop bubbling the event up to its parents
                       */
                      stopPropagation: function() {
                        this.srcEvent.stopPropagation();
                      },
  
                      /**
                       * immediately stop gesture detection
                       * might be useful after a swipe was detected
                       * @return {*}
                       */
                      stopDetect: function() {
                        return ionic.Gestures.detection.stopDetect();
                      }
        };
      }
    };
  
    ionic.Gestures.PointerEvent = {
      /**
       * holds all pointers
       * type {Object}
       */
      pointers: {},
  
      /**
       * get a list of pointers
       * @returns {Array}     touchlist
       */
      getTouchList: function() {
        var self = this;
        var touchlist = [];
  
        // we can use forEach since pointerEvents only is in IE10
        Object.keys(self.pointers).sort().forEach(function(id) {
          touchlist.push(self.pointers[id]);
        });
        return touchlist;
      },
  
      /**
       * update the position of a pointer
       * @param   {String}   type             ionic.Gestures.EVENT_END
       * @param   {Object}   pointerEvent
       */
      updatePointer: function(type, pointerEvent) {
        if(type == ionic.Gestures.EVENT_END) {
          this.pointers = {};
        }
        else {
          pointerEvent.identifier = pointerEvent.pointerId;
          this.pointers[pointerEvent.pointerId] = pointerEvent;
        }
  
        return Object.keys(this.pointers).length;
      },
  
      /**
       * check if ev matches pointertype
       * @param   {String}        pointerType     ionic.Gestures.POINTER_MOUSE
       * @param   {PointerEvent}  ev
       */
      matchType: function(pointerType, ev) {
        if(!ev.pointerType) {
          return false;
        }
  
        var types = {};
        types[ionic.Gestures.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == ionic.Gestures.POINTER_MOUSE);
        types[ionic.Gestures.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == ionic.Gestures.POINTER_TOUCH);
        types[ionic.Gestures.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == ionic.Gestures.POINTER_PEN);
        return types[pointerType];
      },
  
  
      /**
       * get events
       */
      getEvents: function() {
        return [
          'pointerdown MSPointerDown',
        'pointermove MSPointerMove',
        'pointerup pointercancel MSPointerUp MSPointerCancel'
          ];
      },
  
      /**
       * reset the list
       */
      reset: function() {
        this.pointers = {};
      }
    };
  
  
    ionic.Gestures.utils = {
      /**
       * extend method,
       * also used for cloning when dest is an empty object
       * @param   {Object}    dest
       * @param   {Object}    src
       * @param	{Boolean}	merge		do a merge
       * @returns {Object}    dest
       */
      extend: function extend(dest, src, merge) {
        for (var key in src) {
          if(dest[key] !== undefined && merge) {
            continue;
          }
          dest[key] = src[key];
        }
        return dest;
      },
  
  
      /**
       * find if a node is in the given parent
       * used for event delegation tricks
       * @param   {HTMLElement}   node
       * @param   {HTMLElement}   parent
       * @returns {boolean}       has_parent
       */
      hasParent: function(node, parent) {
        while(node){
          if(node == parent) {
            return true;
          }
          node = node.parentNode;
        }
        return false;
      },
  
  
      /**
       * get the center of all the touches
       * @param   {Array}     touches
       * @returns {Object}    center
       */
      getCenter: function getCenter(touches) {
        var valuesX = [], valuesY = [];
  
        for(var t= 0,len=touches.length; t<len; t++) {
          valuesX.push(touches[t].pageX);
          valuesY.push(touches[t].pageY);
        }
  
        return {
          pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
            pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
        };
      },
  
  
      /**
       * calculate the velocity between two points
       * @param   {Number}    delta_time
       * @param   {Number}    delta_x
       * @param   {Number}    delta_y
       * @returns {Object}    velocity
       */
      getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
        return {
          x: Math.abs(delta_x / delta_time) || 0,
          y: Math.abs(delta_y / delta_time) || 0
        };
      },
  
  
      /**
       * calculate the angle between two coordinates
       * @param   {Touch}     touch1
       * @param   {Touch}     touch2
       * @returns {Number}    angle
       */
      getAngle: function getAngle(touch1, touch2) {
        var y = touch2.pageY - touch1.pageY,
        x = touch2.pageX - touch1.pageX;
        return Math.atan2(y, x) * 180 / Math.PI;
      },
  
  
      /**
       * angle to direction define
       * @param   {Touch}     touch1
       * @param   {Touch}     touch2
       * @returns {String}    direction constant, like ionic.Gestures.DIRECTION_LEFT
       */
      getDirection: function getDirection(touch1, touch2) {
        var x = Math.abs(touch1.pageX - touch2.pageX),
        y = Math.abs(touch1.pageY - touch2.pageY);
  
        if(x >= y) {
          return touch1.pageX - touch2.pageX > 0 ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
        }
        else {
          return touch1.pageY - touch2.pageY > 0 ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
        }
      },
  
  
      /**
       * calculate the distance between two touches
       * @param   {Touch}     touch1
       * @param   {Touch}     touch2
       * @returns {Number}    distance
       */
      getDistance: function getDistance(touch1, touch2) {
        var x = touch2.pageX - touch1.pageX,
        y = touch2.pageY - touch1.pageY;
        return Math.sqrt((x*x) + (y*y));
      },
  
  
      /**
       * calculate the scale factor between two touchLists (fingers)
       * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
       * @param   {Array}     start
       * @param   {Array}     end
       * @returns {Number}    scale
       */
      getScale: function getScale(start, end) {
        // need two fingers...
        if(start.length >= 2 && end.length >= 2) {
          return this.getDistance(end[0], end[1]) /
            this.getDistance(start[0], start[1]);
        }
        return 1;
      },
  
  
      /**
       * calculate the rotation degrees between two touchLists (fingers)
       * @param   {Array}     start
       * @param   {Array}     end
       * @returns {Number}    rotation
       */
      getRotation: function getRotation(start, end) {
        // need two fingers
        if(start.length >= 2 && end.length >= 2) {
          return this.getAngle(end[1], end[0]) -
            this.getAngle(start[1], start[0]);
        }
        return 0;
      },
  
  
      /**
       * boolean if the direction is vertical
       * @param    {String}    direction
       * @returns  {Boolean}   is_vertical
       */
      isVertical: function isVertical(direction) {
        return (direction == ionic.Gestures.DIRECTION_UP || direction == ionic.Gestures.DIRECTION_DOWN);
      },
  
  
      /**
       * stop browser default behavior with css class
       * @param   {HtmlElement}   element
       * @param   {Object}        css_class
       */
      stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_class) {
        // changed from making many style changes to just adding a preset classname
        // less DOM manipulations, less code, and easier to control in the CSS side of things
        // hammer.js doesn't come with CSS, but ionic does, which is why we prefer this method
        if(element && element.classList) {
          element.classList.add(css_class);
          element.onselectstart = function() {
            return false;
          };
        }
      }
    };
  
  
    ionic.Gestures.detection = {
      // contains all registred ionic.Gestures.gestures in the correct order
      gestures: [],
  
      // data of the current ionic.Gestures.gesture detection session
      current: null,
  
      // the previous ionic.Gestures.gesture session data
      // is a full clone of the previous gesture.current object
      previous: null,
  
      // when this becomes true, no gestures are fired
      stopped: false,
  
  
      /**
       * start ionic.Gestures.gesture detection
       * @param   {ionic.Gestures.Instance}   inst
       * @param   {Object}            eventData
       */
      startDetect: function startDetect(inst, eventData) {
        // already busy with a ionic.Gestures.gesture detection on an element
        if(this.current) {
          return;
        }
  
        this.stopped = false;
  
        this.current = {
          inst        : inst, // reference to ionic.GesturesInstance we're working for
          startEvent  : ionic.Gestures.utils.extend({}, eventData), // start eventData for distances, timing etc
          lastEvent   : false, // last eventData
          name        : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
        };
  
        this.detect(eventData);
      },
  
  
      /**
       * ionic.Gestures.gesture detection
       * @param   {Object}    eventData
       */
      detect: function detect(eventData) {
        if(!this.current || this.stopped) {
          return;
        }
  
        // extend event data with calculations about scale, distance etc
        eventData = this.extendEventData(eventData);
  
        // instance options
        var inst_options = this.current.inst.options;
  
        // call ionic.Gestures.gesture handlers
        for(var g=0,len=this.gestures.length; g<len; g++) {
          var gesture = this.gestures[g];
  
          // only when the instance options have enabled this gesture
          if(!this.stopped && inst_options[gesture.name] !== false) {
            // if a handler returns false, we stop with the detection
            if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
              this.stopDetect();
              break;
            }
          }
        }
  
        // store as previous event event
        if(this.current) {
          this.current.lastEvent = eventData;
        }
  
        // endevent, but not the last touch, so dont stop
        if(eventData.eventType == ionic.Gestures.EVENT_END && !eventData.touches.length-1) {
          this.stopDetect();
        }
  
        return eventData;
      },
  
  
      /**
       * clear the ionic.Gestures.gesture vars
       * this is called on endDetect, but can also be used when a final ionic.Gestures.gesture has been detected
       * to stop other ionic.Gestures.gestures from being fired
       */
      stopDetect: function stopDetect() {
        // clone current data to the store as the previous gesture
        // used for the double tap gesture, since this is an other gesture detect session
        this.previous = ionic.Gestures.utils.extend({}, this.current);
  
        // reset the current
        this.current = null;
  
        // stopped!
        this.stopped = true;
      },
  
  
      /**
       * extend eventData for ionic.Gestures.gestures
       * @param   {Object}   ev
       * @returns {Object}   ev
       */
      extendEventData: function extendEventData(ev) {
        var startEv = this.current.startEvent;
  
        // if the touches change, set the new touches over the startEvent touches
        // this because touchevents don't have all the touches on touchstart, or the
        // user must place his fingers at the EXACT same time on the screen, which is not realistic
        // but, sometimes it happens that both fingers are touching at the EXACT same time
        if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
          // extend 1 level deep to get the touchlist with the touch objects
          startEv.touches = [];
          for(var i=0,len=ev.touches.length; i<len; i++) {
            startEv.touches.push(ionic.Gestures.utils.extend({}, ev.touches[i]));
          }
        }
  
        var delta_time = ev.timeStamp - startEv.timeStamp,
            delta_x = ev.center.pageX - startEv.center.pageX,
            delta_y = ev.center.pageY - startEv.center.pageY,
            velocity = ionic.Gestures.utils.getVelocity(delta_time, delta_x, delta_y);
  
        ionic.Gestures.utils.extend(ev, {
          deltaTime   : delta_time,
  
          deltaX      : delta_x,
          deltaY      : delta_y,
  
          velocityX   : velocity.x,
          velocityY   : velocity.y,
  
          distance    : ionic.Gestures.utils.getDistance(startEv.center, ev.center),
          angle       : ionic.Gestures.utils.getAngle(startEv.center, ev.center),
          direction   : ionic.Gestures.utils.getDirection(startEv.center, ev.center),
  
          scale       : ionic.Gestures.utils.getScale(startEv.touches, ev.touches),
          rotation    : ionic.Gestures.utils.getRotation(startEv.touches, ev.touches),
  
          startEvent  : startEv
        });
  
        return ev;
      },
  
  
      /**
       * register new gesture
       * @param   {Object}    gesture object, see gestures.js for documentation
       * @returns {Array}     gestures
       */
      register: function register(gesture) {
        // add an enable gesture options if there is no given
        var options = gesture.defaults || {};
        if(options[gesture.name] === undefined) {
          options[gesture.name] = true;
        }
  
        // extend ionic.Gestures default options with the ionic.Gestures.gesture options
        ionic.Gestures.utils.extend(ionic.Gestures.defaults, options, true);
  
        // set its index
        gesture.index = gesture.index || 1000;
  
        // add ionic.Gestures.gesture to the list
        this.gestures.push(gesture);
  
        // sort the list by index
        this.gestures.sort(function(a, b) {
          if (a.index < b.index) {
            return -1;
          }
          if (a.index > b.index) {
            return 1;
          }
          return 0;
        });
  
        return this.gestures;
      }
    };
  
  
    ionic.Gestures.gestures = ionic.Gestures.gestures || {};
  
    /**
     * Custom gestures
     * ==============================
     *
     * Gesture object
     * --------------------
     * The object structure of a gesture:
     *
     * { name: 'mygesture',
     *   index: 1337,
     *   defaults: {
     *     mygesture_option: true
     *   }
     *   handler: function(type, ev, inst) {
     *     // trigger gesture event
     *     inst.trigger(this.name, ev);
     *   }
     * }
  
     * @param   {String}    name
     * this should be the name of the gesture, lowercase
     * it is also being used to disable/enable the gesture per instance config.
     *
     * @param   {Number}    [index=1000]
     * the index of the gesture, where it is going to be in the stack of gestures detection
     * like when you build an gesture that depends on the drag gesture, it is a good
     * idea to place it after the index of the drag gesture.
     *
     * @param   {Object}    [defaults={}]
     * the default settings of the gesture. these are added to the instance settings,
     * and can be overruled per instance. you can also add the name of the gesture,
     * but this is also added by default (and set to true).
     *
     * @param   {Function}  handler
     * this handles the gesture detection of your custom gesture and receives the
     * following arguments:
     *
     *      @param  {Object}    eventData
     *      event data containing the following properties:
     *          timeStamp   {Number}        time the event occurred
     *          target      {HTMLElement}   target element
     *          touches     {Array}         touches (fingers, pointers, mouse) on the screen
     *          pointerType {String}        kind of pointer that was used. matches ionic.Gestures.POINTER_MOUSE|TOUCH
     *          center      {Object}        center position of the touches. contains pageX and pageY
     *          deltaTime   {Number}        the total time of the touches in the screen
     *          deltaX      {Number}        the delta on x axis we haved moved
     *          deltaY      {Number}        the delta on y axis we haved moved
     *          velocityX   {Number}        the velocity on the x
     *          velocityY   {Number}        the velocity on y
     *          angle       {Number}        the angle we are moving
     *          direction   {String}        the direction we are moving. matches ionic.Gestures.DIRECTION_UP|DOWN|LEFT|RIGHT
     *          distance    {Number}        the distance we haved moved
     *          scale       {Number}        scaling of the touches, needs 2 touches
     *          rotation    {Number}        rotation of the touches, needs 2 touches *
     *          eventType   {String}        matches ionic.Gestures.EVENT_START|MOVE|END
     *          srcEvent    {Object}        the source event, like TouchStart or MouseDown *
     *          startEvent  {Object}        contains the same properties as above,
     *                                      but from the first touch. this is used to calculate
     *                                      distances, deltaTime, scaling etc
     *
     *      @param  {ionic.Gestures.Instance}    inst
     *      the instance we are doing the detection for. you can get the options from
     *      the inst.options object and trigger the gesture event by calling inst.trigger
     *
     *
     * Handle gestures
     * --------------------
     * inside the handler you can get/set ionic.Gestures.detectionic.current. This is the current
     * detection sessionic. It has the following properties
     *      @param  {String}    name
     *      contains the name of the gesture we have detected. it has not a real function,
     *      only to check in other gestures if something is detected.
     *      like in the drag gesture we set it to 'drag' and in the swipe gesture we can
     *      check if the current gesture is 'drag' by accessing ionic.Gestures.detectionic.current.name
     *
     *      readonly
     *      @param  {ionic.Gestures.Instance}    inst
     *      the instance we do the detection for
     *
     *      readonly
     *      @param  {Object}    startEvent
     *      contains the properties of the first gesture detection in this sessionic.
     *      Used for calculations about timing, distance, etc.
     *
     *      readonly
     *      @param  {Object}    lastEvent
     *      contains all the properties of the last gesture detect in this sessionic.
     *
     * after the gesture detection session has been completed (user has released the screen)
     * the ionic.Gestures.detectionic.current object is copied into ionic.Gestures.detectionic.previous,
     * this is usefull for gestures like doubletap, where you need to know if the
     * previous gesture was a tap
     *
     * options that have been set by the instance can be received by calling inst.options
     *
     * You can trigger a gesture event by calling inst.trigger("mygesture", event).
     * The first param is the name of your gesture, the second the event argument
     *
     *
     * Register gestures
     * --------------------
     * When an gesture is added to the ionic.Gestures.gestures object, it is auto registered
     * at the setup of the first ionic.Gestures instance. You can also call ionic.Gestures.detectionic.register
     * manually and pass your gesture object as a param
     *
     */
  
    /**
     * Hold
     * Touch stays at the same place for x time
     * events  hold
     */
    ionic.Gestures.gestures.Hold = {
      name: 'hold',
      index: 10,
      defaults: {
        hold_timeout	: 500,
        hold_threshold	: 1
      },
      timer: null,
      handler: function holdGesture(ev, inst) {
        switch(ev.eventType) {
          case ionic.Gestures.EVENT_START:
            // clear any running timers
            clearTimeout(this.timer);
  
            // set the gesture so we can check in the timeout if it still is
            ionic.Gestures.detection.current.name = this.name;
  
            // set timer and if after the timeout it still is hold,
            // we trigger the hold event
            this.timer = setTimeout(function() {
              if(ionic.Gestures.detection.current.name == 'hold') {
                ionic.tap.cancelClick();
                inst.trigger('hold', ev);
              }
            }, inst.options.hold_timeout);
            break;
  
            // when you move or end we clear the timer
          case ionic.Gestures.EVENT_MOVE:
            if(ev.distance > inst.options.hold_threshold) {
              clearTimeout(this.timer);
            }
            break;
  
          case ionic.Gestures.EVENT_END:
            clearTimeout(this.timer);
            break;
        }
      }
    };
  
  
    /**
     * Tap/DoubleTap
     * Quick touch at a place or double at the same place
     * events  tap, doubletap
     */
    ionic.Gestures.gestures.Tap = {
      name: 'tap',
      index: 100,
      defaults: {
        tap_max_touchtime	: 250,
        tap_max_distance	: 10,
        tap_always			: true,
        doubletap_distance	: 20,
        doubletap_interval	: 300
      },
      handler: function tapGesture(ev, inst) {
        if(ev.eventType == ionic.Gestures.EVENT_END && ev.srcEvent.type != 'touchcancel') {
          // previous gesture, for the double tap since these are two different gesture detections
          var prev = ionic.Gestures.detection.previous,
          did_doubletap = false;
  
          // when the touchtime is higher then the max touch time
          // or when the moving distance is too much
          if(ev.deltaTime > inst.options.tap_max_touchtime ||
              ev.distance > inst.options.tap_max_distance) {
                return;
              }
  
          // check if double tap
          if(prev && prev.name == 'tap' &&
              (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
              ev.distance < inst.options.doubletap_distance) {
                inst.trigger('doubletap', ev);
                did_doubletap = true;
              }
  
          // do a single tap
          if(!did_doubletap || inst.options.tap_always) {
            ionic.Gestures.detection.current.name = 'tap';
            inst.trigger('tap', ev);
          }
        }
      }
    };
  
  
    /**
     * Swipe
     * triggers swipe events when the end velocity is above the threshold
     * events  swipe, swipeleft, swiperight, swipeup, swipedown
     */
    ionic.Gestures.gestures.Swipe = {
      name: 'swipe',
      index: 40,
      defaults: {
        // set 0 for unlimited, but this can conflict with transform
        swipe_max_touches  : 1,
        swipe_velocity     : 0.7
      },
      handler: function swipeGesture(ev, inst) {
        if(ev.eventType == ionic.Gestures.EVENT_END) {
          // max touches
          if(inst.options.swipe_max_touches > 0 &&
              ev.touches.length > inst.options.swipe_max_touches) {
                return;
              }
  
          // when the distance we moved is too small we skip this gesture
          // or we can be already in dragging
          if(ev.velocityX > inst.options.swipe_velocity ||
              ev.velocityY > inst.options.swipe_velocity) {
                // trigger swipe events
                inst.trigger(this.name, ev);
                inst.trigger(this.name + ev.direction, ev);
              }
        }
      }
    };
  
  
    /**
     * Drag
     * Move with x fingers (default 1) around on the page. Blocking the scrolling when
     * moving left and right is a good practice. When all the drag events are blocking
     * you disable scrolling on that area.
     * events  drag, drapleft, dragright, dragup, dragdown
     */
    ionic.Gestures.gestures.Drag = {
      name: 'drag',
      index: 50,
      defaults: {
        drag_min_distance : 10,
        // Set correct_for_drag_min_distance to true to make the starting point of the drag
        // be calculated from where the drag was triggered, not from where the touch started.
        // Useful to avoid a jerk-starting drag, which can make fine-adjustments
        // through dragging difficult, and be visually unappealing.
        correct_for_drag_min_distance : true,
        // set 0 for unlimited, but this can conflict with transform
        drag_max_touches  : 1,
        // prevent default browser behavior when dragging occurs
        // be careful with it, it makes the element a blocking element
        // when you are using the drag gesture, it is a good practice to set this true
        drag_block_horizontal   : true,
        drag_block_vertical     : true,
        // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
        // It disallows vertical directions if the initial direction was horizontal, and vice versa.
        drag_lock_to_axis       : false,
        // drag lock only kicks in when distance > drag_lock_min_distance
        // This way, locking occurs only when the distance has become large enough to reliably determine the direction
        drag_lock_min_distance : 25
      },
      triggered: false,
      handler: function dragGesture(ev, inst) {
        // current gesture isnt drag, but dragged is true
        // this means an other gesture is busy. now call dragend
        if(ionic.Gestures.detection.current.name != this.name && this.triggered) {
          inst.trigger(this.name +'end', ev);
          this.triggered = false;
          return;
        }
  
        // max touches
        if(inst.options.drag_max_touches > 0 &&
            ev.touches.length > inst.options.drag_max_touches) {
              return;
            }
  
        switch(ev.eventType) {
          case ionic.Gestures.EVENT_START:
            this.triggered = false;
            break;
  
          case ionic.Gestures.EVENT_MOVE:
            // when the distance we moved is too small we skip this gesture
            // or we can be already in dragging
            if(ev.distance < inst.options.drag_min_distance &&
                ionic.Gestures.detection.current.name != this.name) {
                  return;
                }
  
            // we are dragging!
            if(ionic.Gestures.detection.current.name != this.name) {
              ionic.Gestures.detection.current.name = this.name;
              if (inst.options.correct_for_drag_min_distance) {
                // When a drag is triggered, set the event center to drag_min_distance pixels from the original event center.
                // Without this correction, the dragged distance would jumpstart at drag_min_distance pixels instead of at 0.
                // It might be useful to save the original start point somewhere
                var factor = Math.abs(inst.options.drag_min_distance/ev.distance);
                ionic.Gestures.detection.current.startEvent.center.pageX += ev.deltaX * factor;
                ionic.Gestures.detection.current.startEvent.center.pageY += ev.deltaY * factor;
  
                // recalculate event data using new start point
                ev = ionic.Gestures.detection.extendEventData(ev);
              }
            }
  
            // lock drag to axis?
            if(ionic.Gestures.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
              ev.drag_locked_to_axis = true;
            }
            var last_direction = ionic.Gestures.detection.current.lastEvent.direction;
            if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
              // keep direction on the axis that the drag gesture started on
              if(ionic.Gestures.utils.isVertical(last_direction)) {
                ev.direction = (ev.deltaY < 0) ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
              }
              else {
                ev.direction = (ev.deltaX < 0) ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
              }
            }
  
            // first time, trigger dragstart event
            if(!this.triggered) {
              inst.trigger(this.name +'start', ev);
              this.triggered = true;
            }
  
            // trigger normal event
            inst.trigger(this.name, ev);
  
            // direction event, like dragdown
            inst.trigger(this.name + ev.direction, ev);
  
            // block the browser events
            if( (inst.options.drag_block_vertical && ionic.Gestures.utils.isVertical(ev.direction)) ||
                (inst.options.drag_block_horizontal && !ionic.Gestures.utils.isVertical(ev.direction))) {
                  ev.preventDefault();
                }
            break;
  
          case ionic.Gestures.EVENT_END:
            // trigger dragend
            if(this.triggered) {
              inst.trigger(this.name +'end', ev);
            }
  
            this.triggered = false;
            break;
        }
      }
    };
  
  
    /**
     * Transform
     * User want to scale or rotate with 2 fingers
     * events  transform, pinch, pinchin, pinchout, rotate
     */
    ionic.Gestures.gestures.Transform = {
      name: 'transform',
      index: 45,
      defaults: {
        // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
        transform_min_scale     : 0.01,
        // rotation in degrees
        transform_min_rotation  : 1,
        // prevent default browser behavior when two touches are on the screen
        // but it makes the element a blocking element
        // when you are using the transform gesture, it is a good practice to set this true
        transform_always_block  : false
      },
      triggered: false,
      handler: function transformGesture(ev, inst) {
        // current gesture isnt drag, but dragged is true
        // this means an other gesture is busy. now call dragend
        if(ionic.Gestures.detection.current.name != this.name && this.triggered) {
          inst.trigger(this.name +'end', ev);
          this.triggered = false;
          return;
        }
  
        // atleast multitouch
        if(ev.touches.length < 2) {
          return;
        }
  
        // prevent default when two fingers are on the screen
        if(inst.options.transform_always_block) {
          ev.preventDefault();
        }
  
        switch(ev.eventType) {
          case ionic.Gestures.EVENT_START:
            this.triggered = false;
            break;
  
          case ionic.Gestures.EVENT_MOVE:
            var scale_threshold = Math.abs(1-ev.scale);
            var rotation_threshold = Math.abs(ev.rotation);
  
            // when the distance we moved is too small we skip this gesture
            // or we can be already in dragging
            if(scale_threshold < inst.options.transform_min_scale &&
                rotation_threshold < inst.options.transform_min_rotation) {
                  return;
                }
  
            // we are transforming!
            ionic.Gestures.detection.current.name = this.name;
  
            // first time, trigger dragstart event
            if(!this.triggered) {
              inst.trigger(this.name +'start', ev);
              this.triggered = true;
            }
  
            inst.trigger(this.name, ev); // basic transform event
  
            // trigger rotate event
            if(rotation_threshold > inst.options.transform_min_rotation) {
              inst.trigger('rotate', ev);
            }
  
            // trigger pinch event
            if(scale_threshold > inst.options.transform_min_scale) {
              inst.trigger('pinch', ev);
              inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
            }
            break;
  
          case ionic.Gestures.EVENT_END:
            // trigger dragend
            if(this.triggered) {
              inst.trigger(this.name +'end', ev);
            }
  
            this.triggered = false;
            break;
        }
      }
    };
  
  
    /**
     * Touch
     * Called as first, tells the user has touched the screen
     * events  touch
     */
    ionic.Gestures.gestures.Touch = {
      name: 'touch',
      index: -Infinity,
      defaults: {
        // call preventDefault at touchstart, and makes the element blocking by
        // disabling the scrolling of the page, but it improves gestures like
        // transforming and dragging.
        // be careful with using this, it can be very annoying for users to be stuck
        // on the page
        prevent_default: false,
  
        // disable mouse events, so only touch (or pen!) input triggers events
        prevent_mouseevents: false
      },
      handler: function touchGesture(ev, inst) {
        if(inst.options.prevent_mouseevents && ev.pointerType == ionic.Gestures.POINTER_MOUSE) {
          ev.stopDetect();
          return;
        }
  
        if(inst.options.prevent_default) {
          ev.preventDefault();
        }
  
        if(ev.eventType ==  ionic.Gestures.EVENT_START) {
          inst.trigger(this.name, ev);
        }
      }
    };
  
  
    /**
     * Release
     * Called as last, tells the user has released the screen
     * events  release
     */
    ionic.Gestures.gestures.Release = {
      name: 'release',
      index: Infinity,
      handler: function releaseGesture(ev, inst) {
        if(ev.eventType ==  ionic.Gestures.EVENT_END) {
          inst.trigger(this.name, ev);
        }
      }
    };
  })(window.ionic);
  