text: // drag
  ( function( window, factory ) {
    // universal module definition
    /* jshint strict: false */
    if ( typeof define == 'function' && define.amd ) {
      // AMD
      define( [
        './flickity',
        'unidragger/unidragger',
        'fizzy-ui-utils/utils'
      ], function( Flickity, Unidragger, utils ) {
        return factory( window, Flickity, Unidragger, utils );
      });
    } else if ( typeof module == 'object' && module.exports ) {
      // CommonJS
      module.exports = factory(
        window,
        require('./flickity'),
        require('unidragger'),
        require('fizzy-ui-utils')
      );
    } else {
      // browser global
      window.Flickity = factory(
        window,
        window.Flickity,
        window.Unidragger,
        window.fizzyUIUtils
      );
    }
  
  }( window, function factory( window, Flickity, Unidragger, utils ) {
  
  'use strict';
  
  // ----- defaults ----- //
  
  utils.extend( Flickity.defaults, {
    draggable: true,
    dragThreshold: 3,
  });
  
  // ----- create ----- //
  
  Flickity.createMethods.push('_createDrag');
  
  // -------------------------- drag prototype -------------------------- //
  
  var proto = Flickity.prototype;
  utils.extend( proto, Unidragger.prototype );
  
  // --------------------------  -------------------------- //
  
  var isTouch = 'createTouch' in document;
  var isTouchmoveScrollCanceled = false;
  
  proto._createDrag = function() {
    this.on( 'activate', this.bindDrag );
    this.on( 'uiChange', this._uiChangeDrag );
    this.on( 'childUIPointerDown', this._childUIPointerDownDrag );
    this.on( 'deactivate', this.unbindDrag );
    // HACK - add seemingly innocuous handler to fix iOS 10 scroll behavior
    // #457, RubaXa/Sortable#973
    if ( isTouch && !isTouchmoveScrollCanceled ) {
      window.addEventListener( 'touchmove', function() {});
      isTouchmoveScrollCanceled = true;
    }
  };
  
  proto.bindDrag = function() {
    if ( !this.options.draggable || this.isDragBound ) {
      return;
    }
    this.element.classList.add('is-draggable');
    this.handles = [ this.viewport ];
    this.bindHandles();
    this.isDragBound = true;
  };
  
  proto.unbindDrag = function() {
    if ( !this.isDragBound ) {
      return;
    }
    this.element.classList.remove('is-draggable');
    this.unbindHandles();
    delete this.isDragBound;
  };
  
  proto._uiChangeDrag = function() {
    delete this.isFreeScrolling;
  };
  
  proto._childUIPointerDownDrag = function( event ) {
    event.preventDefault();
    this.pointerDownFocus( event );
  };
  
  // -------------------------- pointer events -------------------------- //
  
  // nodes that have text fields
  var cursorNodes = {
    TEXTAREA: true,
    INPUT: true,
    OPTION: true,
  };
  
  // input types that do not have text fields
  var clickTypes = {
    radio: true,
    checkbox: true,
    button: true,
    submit: true,
    image: true,
    file: true,
  };
  
  proto.pointerDown = function( event, pointer ) {
    // dismiss inputs with text fields. #403, #404
    var isCursorInput = cursorNodes[ event.target.nodeName ] &&
      !clickTypes[ event.target.type ];
    if ( isCursorInput ) {
      // reset pointerDown logic
      this.isPointerDown = false;
      delete this.pointerIdentifier;
      return;
    }
  
    this._dragPointerDown( event, pointer );
  
    // kludge to blur focused inputs in dragger
    var focused = document.activeElement;
    if ( focused && focused.blur && focused != this.element &&
      // do not blur body for IE9 & 10, #117
      focused != document.body ) {
      focused.blur();
    }
    this.pointerDownFocus( event );
    // stop if it was moving
    this.dragX = this.x;
    this.viewport.classList.add('is-pointer-down');
    // bind move and end events
    this._bindPostStartEvents( event );
    // track scrolling
    this.pointerDownScroll = getScrollPosition();
    window.addEventListener( 'scroll', this );
  
    this.dispatchEvent( 'pointerDown', event, [ pointer ] );
  };
  
  var touchStartEvents = {
    touchstart: true,
    MSPointerDown: true
  };
  
  var focusNodes = {
    INPUT: true,
    SELECT: true
  };
  
  proto.pointerDownFocus = function( event ) {
    // focus element, if not touch, and its not an input or select
    if ( !this.options.accessibility || touchStartEvents[ event.type ] ||
        focusNodes[ event.target.nodeName ] ) {
      return;
    }
    var prevScrollY = window.pageYOffset;
    this.element.focus();
    // hack to fix scroll jump after focus, #76
    if ( window.pageYOffset != prevScrollY ) {
      window.scrollTo( window.pageXOffset, prevScrollY );
    }
  };
  
  proto.canPreventDefaultOnPointerDown = function( event ) {
    // prevent default, unless touchstart or <select>
    var isTouchstart = event.type == 'touchstart';
    var targetNodeName = event.target.nodeName;
    return !isTouchstart && targetNodeName != 'SELECT';
  };
  
  // ----- move ----- //
  
  proto.hasDragStarted = function( moveVector ) {
    return Math.abs( moveVector.x ) > this.options.dragThreshold;
  };
  
  // ----- up ----- //
  
  proto.pointerUp = function( event, pointer ) {
    delete this.isTouchScrolling;
    this.viewport.classList.remove('is-pointer-down');
    this.dispatchEvent( 'pointerUp', event, [ pointer ] );
    this._dragPointerUp( event, pointer );
  };
  
  proto.pointerDone = function() {
    window.removeEventListener( 'scroll', this );
    delete this.pointerDownScroll;
  };
  
  // -------------------------- dragging -------------------------- //
  
  proto.dragStart = function( event, pointer ) {
    this.dragStartPosition = this.x;
    this.startAnimation();
    window.removeEventListener( 'scroll', this );
    this.dispatchEvent( 'dragStart', event, [ pointer ] );
  };
  
  proto.pointerMove = function( event, pointer ) {
    var moveVector = this._dragPointerMove( event, pointer );
    this.dispatchEvent( 'pointerMove', event, [ pointer, moveVector ] );
    this._dragMove( event, pointer, moveVector );
  };
  
  proto.dragMove = function( event, pointer, moveVector ) {
    event.preventDefault();
  
    this.previousDragX = this.dragX;
    // reverse if right-to-left
    var direction = this.options.rightToLeft ? -1 : 1;
    var dragX = this.dragStartPosition + moveVector.x * direction;
  
    if ( !this.options.wrapAround && this.slides.length ) {
      // slow drag
      var originBound = Math.max( -this.slides[0].target, this.dragStartPosition );
      dragX = dragX > originBound ? ( dragX + originBound ) * 0.5 : dragX;
      var endBound = Math.min( -this.getLastSlide().target, this.dragStartPosition );
      dragX = dragX < endBound ? ( dragX + endBound ) * 0.5 : dragX;
    }
  
    this.dragX = dragX;
  
    this.dragMoveTime = new Date();
    this.dispatchEvent( 'dragMove', event, [ pointer, moveVector ] );
  };
  
  proto.dragEnd = function( event, pointer ) {
    if ( this.options.freeScroll ) {
      this.isFreeScrolling = true;
    }
    // set selectedIndex based on where flick will end up
    var index = this.dragEndRestingSelect();
  
    if ( this.options.freeScroll && !this.options.wrapAround ) {
      // if free-scroll & not wrap around
      // do not free-scroll if going outside of bounding slides
      // so bounding slides can attract slider, and keep it in bounds
      var restingX = this.getRestingPosition();
      this.isFreeScrolling = -restingX > this.slides[0].target &&
        -restingX < this.getLastSlide().target;
    } else if ( !this.options.freeScroll && index == this.selectedIndex ) {
      // boost selection if selected index has not changed
      index += this.dragEndBoostSelect();
    }
    delete this.previousDragX;
    // apply selection
    // TODO refactor this, selecting here feels weird
    // HACK, set flag so dragging stays in correct direction
    this.isDragSelect = this.options.wrapAround;
    this.select( index );
    delete this.isDragSelect;
    this.dispatchEvent( 'dragEnd', event, [ pointer ] );
  };
  
  proto.dragEndRestingSelect = function() {
    var restingX = this.getRestingPosition();
    // how far away from selected slide
    var distance = Math.abs( this.getSlideDistance( -restingX, this.selectedIndex ) );
    // get closet resting going up and going down
    var positiveResting = this._getClosestResting( restingX, distance, 1 );
    var negativeResting = this._getClosestResting( restingX, distance, -1 );
    // use closer resting for wrap-around
    var index = positiveResting.distance < negativeResting.distance ?
      positiveResting.index : negativeResting.index;
    return index;
  };
  
  /**
   * given resting X and distance to selected cell
   * get the distance and index of the closest cell
   * @param {Number} restingX - estimated post-flick resting position
   * @param {Number} distance - distance to selected cell
   * @param {Integer} increment - +1 or -1, going up or down
   * @returns {Object} - { distance: {Number}, index: {Integer} }
   */
  proto._getClosestResting = function( restingX, distance, increment ) {
    var index = this.selectedIndex;
    var minDistance = Infinity;
    var condition = this.options.contain && !this.options.wrapAround ?
      // if contain, keep going if distance is equal to minDistance
      function( d, md ) { return d <= md; } : function( d, md ) { return d < md; };
    while ( condition( distance, minDistance ) ) {
      // measure distance to next cell
      index += increment;
      minDistance = distance;
      distance = this.getSlideDistance( -restingX, index );
      if ( distance === null ) {
        break;
      }
      distance = Math.abs( distance );
    }
    return {
      distance: minDistance,
      // selected was previous index
      index: index - increment
    };
  };
  
  /**
   * measure distance between x and a slide target
   * @param {Number} x
   * @param {Integer} index - slide index
   */
  proto.getSlideDistance = function( x, index ) {
    var len = this.slides.length;
    // wrap around if at least 2 slides
    var isWrapAround = this.options.wrapAround && len > 1;
    var slideIndex = isWrapAround ? utils.modulo( index, len ) : index;
    var slide = this.slides[ slideIndex ];
    if ( !slide ) {
      return null;
    }
    // add distance for wrap-around slides
    var wrap = isWrapAround ? this.slideableWidth * Math.floor( index / len ) : 0;
    return x - ( slide.target + wrap );
  };
  
  proto.dragEndBoostSelect = function() {
    // do not boost if no previousDragX or dragMoveTime
    if ( this.previousDragX === undefined || !this.dragMoveTime ||
      // or if drag was held for 100 ms
      new Date() - this.dragMoveTime > 100 ) {
      return 0;
    }
  
    var distance = this.getSlideDistance( -this.dragX, this.selectedIndex );
    var delta = this.previousDragX - this.dragX;
    if ( distance > 0 && delta > 0 ) {
      // boost to next if moving towards the right, and positive velocity
      return 1;
    } else if ( distance < 0 && delta < 0 ) {
      // boost to previous if moving towards the left, and negative velocity
      return -1;
    }
    return 0;
  };
  
  // ----- staticClick ----- //
  
  proto.staticClick = function( event, pointer ) {
    // get clickedCell, if cell was clicked
    var clickedCell = this.getParentCell( event.target );
    var cellElem = clickedCell && clickedCell.element;
    var cellIndex = clickedCell && this.cells.indexOf( clickedCell );
    this.dispatchEvent( 'staticClick', event, [ pointer, cellElem, cellIndex ] );
  };
  
  // ----- scroll ----- //
  
  proto.onscroll = function() {
    var scroll = getScrollPosition();
    var scrollMoveX = this.pointerDownScroll.x - scroll.x;
    var scrollMoveY = this.pointerDownScroll.y - scroll.y;
    // cancel click/tap if scroll is too much
    if ( Math.abs( scrollMoveX ) > 3 || Math.abs( scrollMoveY ) > 3 ) {
      this._pointerDone();
    }
  };
  
  // ----- utils ----- //
  
  function getScrollPosition() {
    return {
      x: window.pageXOffset,
      y: window.pageYOffset
    };
  }
  
  // -----  ----- //
  
  return Flickity;
  
  }));
  