    /*
     * This is the base scroller implementation.
     *
     * Please look at the {@link Ext.scroll.Scroller} documentation for more information.
     */
    Ext.define('Ext.scroll.scroller.Abstract', {

        extend: 'Ext.Evented',

        requires: [
            'Ext.fx.easing.BoundMomentum',
            'Ext.fx.easing.EaseOut',
            'Ext.util.SizeMonitor'
        ],

    // Document all members of this class as being part of another class
    /**
     * @class Ext.scroll.Scroller
     */

        /**
         * @event maxpositionchange
         * Fires whenever the maximum position has changed
         * @param {Ext.scroll.Scroller} this
         * @param {Number} maxPosition The new maximum position
         */

        /**
         * @event refresh
         * Fires whenever the Scroller is refreshed
         * @param {Ext.scroll.Scroller} this
         */

        /**
         * @event scrollstart
         * Fires whenever the scrolling is started
         * @param {Ext.scroll.Scroller} this
         */

        /**
         * @event scrollend
         * Fires whenever the scrolling is ended
         * @param {Ext.scroll.Scroller} this
         * @param {Object} position The new position object
         */

        /**
         * @event scroll
         * Fires whenever the Scroller is scrolled
         * @param {Ext.scroll.Scroller} this
         * @param {Number} x The new x position
         * @param {Number} y The new y position
         */

        config: {
            /**
             * @cfg {String} direction
             * Possible values: 'auto', 'vertical', 'horizontal', or 'both'
             * @accessor
             */
            direction: 'auto',

            /**
             * @cfg {Number} fps
             * The desired fps of the deceleration.
             * @accessor
             */
            fps: 60,

            /**
             * @cfg {Number} snap
             * The size to snap to vertically.
             * @accessor
             */
            snap: null,

            /**
             * @cfg {Boolean} disabled
             * Whether or not this component is disabled
             * @accessor
             */
            disabled: null,

            /**
             * @cfg {Boolean} directionLock
             * True to lock the direction of the scroller when the user starts scrolling.
             * This is useful when putting a scroller inside a scroller or a {@link Ext.Carousel}.
             * @accessor
             */
            directionLock: false,

            /**
             * @cfg {Object} momentumEasing
             * A valid config for {@link Ext.fx.easing.BoundMomentum}. The default value is:
             *
             *     {
             *         momentum: {
             *             acceleration: 30,
             *             friction: 0.5
             *         },
             *         bounce: {
             *             acceleration: 30,
             *             springTension: 0.3
             *         }
             *     }
             *
             * Note that supplied object will be recursively merged with the default object. For example: you can simply
             * pass this to change the momentum acceleration:
             *
             *     {
             *         momentum: {
             *             acceleration: 10
             *         }
             *     }
             *
             * ...and it will still take the scrollers default values.
             * @accessor
             */
            momentumEasing: {
                momentum: {
                    acceleration: 30,
                    friction: 0.5
                },

                bounce: {
                    acceleration: 30,
                    springTension: 0.3
                },

                minVelocity: 0.2
            },

            /**
             * @cfg
             * @private
             */
            element: null,

            /**
             * @cfg
             * @private
             */
            snapEasing: {
                duration: 400,
                exponent: 4
            },

            /**
             * @cfg
             * @private
             */
            outOfBoundRestrictFactor: 0.5,

            /**
             * @cfg
             * @private
             */
            startMomentumResetTime: 300,

            /**
             * @cfg
             * @private
             */
            maxAbsoluteVelocity: 2,

            /**
             * @cfg
             * @private
             */
            containerSize: 'auto',

            /**
             * @cfg
             * @private
             */
            containerScrollSize: 'auto',

            /**
             * @cfg
             * @private
             */
            size: 'auto',

            /**
             * @cfg
             * @private
             */
            snapOffset: {
                x: 0,
                y: 0
            },

            /**
             * @cfg
             * @private
             */
            autoRefresh: true,

            /**
             * @cfg
             * @private
             */
            cls: Ext.baseCSSPrefix + 'scroll-scroller',

            /**
             * @cfg
             * @private
             */
            containerCls: Ext.baseCSSPrefix + 'scroll-container',

            /**
             * @cfg
             * @private
             */
            initialOffset: {
                x: 0,
                y: 0
            }
        },

        dragStartTime: 0,

        dragEndTime: 0,

        activeEasing: null,

        isDragging: false,

        isAnimating: false,

        /**
         * @private
         */
        constructor: function(config) {
            var element = config && config.element;

            this.doAnimationFrame = Ext.Function.bind(this.doAnimationFrame, this);

            this.listeners = {
                scope: this,
                touchstart: 'onTouchStart',
                dragstart : 'onDragStart',
                drag      : 'onDrag',
                dragend   : 'onDragEnd'
            };

            this.minPosition = { x: 0, y: 0 };

            this.startPosition = { x: 0, y: 0 };

            this.size = { x: 0, y: 0 };

            this.position = { x: 0, y: 0 };

            this.velocity = { x: 0, y: 0 };

            this.isAxisEnabledFlags = { x: false, y: false };

            this.activeEasing = { x: null, y: null };

            this.flickStartPosition = { x: 0, y: 0 };

            this.flickStartTime = { x: 0, y: 0 };

            this.lastDragPosition = { x: 0, y: 0 };

            this.dragDirection = { x: 0, y: 0};

            this.initialConfig = config;

            if (element) {
                delete config.element;
                this.setElement(element);
            }

            return this;
        },

        /**
         * @private
         */
        applyElement: function(element) {
            if (!element) {
                return;
            }

            return Ext.get(element);
        },

        /**
         * @private
         */
        updateElement: function(element) {
            this.initialize();

            element.addCls(this.getCls());

            this.onAfterInitialized();

            return this;
        },

        /**
         * @private
         */
        onAfterInitialized: function() {
            if (!this.getDisabled()) {
                this.attachListeneners();
            }

            this.onConfigUpdate(['containerSize', 'size'], 'refreshMaxPosition');

            this.on('maxpositionchange', 'snapToBoundary');
            this.on('minpositionchange', 'snapToBoundary');
        },

        /**
         * @private
         */
        attachListeneners: function() {
            this.getContainer().on(this.listeners);
        },

        /**
         * @private
         */
        detachListeners: function() {
            this.getContainer().un(this.listeners);
        },

        /**
         * @private
         */
        updateDisabled: function(disabled) {
            if (disabled) {
                this.detachListeners();
            }
            else {
                this.attachListeneners();
            }
        },

        updateInitialOffset: function(initialOffset) {
            var position = this.position;

            position.x = initialOffset.x;
            position.y = initialOffset.y;

            this.doScrollTo(position.x, position.y);
        },

        /**
         * @private
         */
        updateFps: function(fps) {
            this.animationInterval = 1000 / fps;
        },

        /**
         * @private
         */
        applyDirection: function(direction) {
            var minPosition = this.getMinPosition(),
                maxPosition = this.getMaxPosition(),
                isHorizontal, isVertical;

            this.givenDirection = direction;

            if (direction === 'auto') {
                isHorizontal = maxPosition.x > minPosition.x;
                isVertical = maxPosition.y > minPosition.y;

                if (isHorizontal && isVertical) {
                    direction = 'both';
                }
                else if (isHorizontal) {
                    direction = 'horizontal';
                }
                else {
                    direction = 'vertical';
                }
            }

            return direction;
        },

        /**
         * @private
         */
        updateDirection: function(direction) {
            var isAxisEnabled = this.isAxisEnabledFlags;

            isAxisEnabled.x = (direction === 'both' || direction === 'horizontal');
            isAxisEnabled.y = (direction === 'both' || direction === 'vertical');
        },

        /**
         * Returns true if a specified axis is enabled
         * @param {String} axis The axis to check (`x` or `y`).
         * @return {Boolean} True if the axis is enabled
         */
        isAxisEnabled: function(axis) {
            this.getDirection();

            return this.isAxisEnabledFlags[axis];
        },

        /**
         * @private
         */
        applyMomentumEasing: function(easing) {
            var defaultEasingClass = Ext.fx.easing.BoundMomentum;

            if (!(easing instanceof Ext.fx.easing.Abstract)) {
                return {
                    x: new defaultEasingClass(easing),
                    y: new defaultEasingClass(easing)
                };
            }

            return {
                x: easing,
                y: easing.clone()
            };
        },

        /**
         * @private
         */
        applySnapEasing: function(easing) {
            var defaultEasingClass = Ext.fx.easing.EaseOut;

            if (!(easing instanceof Ext.fx.easing.Abstract)) {
                return {
                    x: new defaultEasingClass(easing),
                    y: new defaultEasingClass(easing)
                };
            }

            return {
                x: easing,
                y: easing.clone()
            };
        },

        /**
         * @private
         */
        getMinPosition: function() {
            var minPosition = this.minPosition;

            if (!minPosition) {
                this.minPosition = minPosition = {
                    x: 0,
                    y: 0
                };

                this.fireEvent('minpositionchange', this, minPosition);
            }

            return minPosition;
        },

        /**
         * @private
         */
        getMaxPosition: function() {
            var maxPosition = this.maxPosition,
                size, containerSize;

            if (!maxPosition) {
                size = this.getSize();
                containerSize = this.getContainerSize();

                this.maxPosition = maxPosition = {
                    x: Math.max(0, size.x - containerSize.x),
                    y: Math.max(0, size.y - containerSize.y)
                };

                this.fireEvent('maxpositionchange', this, maxPosition);
            }

            return maxPosition;
        },

        /**
         * @private
         */
        refreshMaxPosition: function() {
            this.maxPosition = null;
            this.getMaxPosition();
        },

        /**
         * @private
         */
        applyContainerSize: function(size) {
            var containerDom, x, y;

            this.givenContainerSize = size;

            if (size === 'auto') {
                containerDom = this.getContainer().dom;

                x = containerDom.offsetWidth;
                y = containerDom.offsetHeight;
            }
            else {
                x = size.x;
                y = size.y;
            }

            return {
                x: x,
                y: y
            };
        },

        /**
         * @private
         */
        applySize: function(size) {
            var dom, x, y;

            this.givenSize = size;

            if (size === 'auto') {
                dom = this.getElement().dom;

                x = dom.offsetWidth;
                y = dom.offsetHeight;
            }
            else {
                x = size.x;
                y = size.y;
            }

            return {
                x: x,
                y: y
            };
        },

        /**
         * @private
         */
        applyContainerScrollSize: function(size) {
            var containerDom, x, y;

            this.givenContainerScrollSize = size;

            if (size === 'auto') {
                containerDom = this.getContainer().dom;

                x = containerDom.scrollWidth;
                y = containerDom.scrollHeight;
            }
            else {
                x = size.x;
                y = size.y;
            }

            return {
                x: x,
                y: y
            };
        },

        /**
         * @private
         */
        updateAutoRefresh: function(autoRefresh) {
            var SizeMonitor = Ext.util.SizeMonitor;

            if (autoRefresh) {
                this.sizeMonitors = {
                    element: new SizeMonitor({
                        element: this.getElement(),
                        callback: this.doRefresh,
                        scope: this
                    }),
                    container: new SizeMonitor({
                        element: this.getContainer(),
                        callback: this.doRefresh,
                        scope: this
                    })
                };
            }
        },

        /**
         * @private
         * Returns the container for this scroller
         */
        getContainer: function() {
            var container = this.container;

            if (!container) {
                this.container = container = this.getElement().getParent();
                container.addCls(this.getContainerCls());
            }

            return container;
        },

        /**
         * @private
         */
        doRefresh: function() {
            this.stopAnimation();

            this.setSize(this.givenSize);
            this.setContainerSize(this.givenContainerSize);
            this.setContainerScrollSize(this.givenContainerScrollSize);
            this.setDirection(this.givenDirection);

            this.fireEvent('refresh', this);
        },

        /**
         * Refreshes the scrollers sizes. Useful if the content size has changed and the scroller has somehow missed it.
         * @return {Ext.scroll.Scroller} this
         */
        refresh: function() {
            var sizeMonitors = this.sizeMonitors;

            if (sizeMonitors) {
                sizeMonitors.element.refresh();
                sizeMonitors.container.refresh();
            }

            this.doRefresh();

            return this;
        },

        /**
         * Scrolls to a given location with no animation.
         * Use {@link #scrollToAnimated} to scroll with animation.
         * @param {Number} x The value to scroll on the x axis
         * @param {Number} y The value to scroll on the y axis
         * @return {Ext.scroll.Scroller} this
         */
        scrollTo: function(x, y) {
            //<deprecated product=touch since=2.0>
            if (typeof x != 'number' && arguments.length === 1) {
                //<debug warn>
                Ext.Logger.deprecate("Calling scrollTo() with an object argument is deprecated, " +
                    "please pass x and y arguments instead", this);
                //</debug>

                y = x.y;
                x = x.x;
            }
            //</deprecated>

            var position = this.position,
                positionChanged = false,
                actualX = null,
                actualY = null;

            if (this.isAxisEnabled('x')) {
                if (typeof x != 'number') {
                    x = position.x;
                }
                else {
                    if (position.x !== x) {
                        position.x = x;
                        positionChanged = true;
                    }
                }

                actualX = x;
            }

            if (this.isAxisEnabled('y')) {
                if (typeof y != 'number') {
                    y = position.y;
                }
                else {
                    if (position.y !== y) {
                        position.y = y;
                        positionChanged = true;
                    }
                }

                actualY = y;
            }

            if (positionChanged) {
                this.fireEvent('scroll', this, position.x, position.y);
                this.doScrollTo(actualX, actualY);
            }

            return this;
        },

        /**
         * @private
         */
        scrollToTop: function() {
            var initialOffset = this.getInitialOffset();

            this.scrollTo(initialOffset.x, initialOffset.y);
        },

        /**
         * Scrolls to a specified x+y location using animation.
         * @param {Number} x The value to scroll on the x axis
         * @param {Number} y The value to scroll on the y axis
         * @return {Ext.scroll.Scroller} this
         */
        scrollToAnimated: function(x, y) {
            var currentPosition = this.position,
                easingX, easingY;

            easingX = this.getSnapEasing().x;
            easingX.setConfig({
                startTime : Ext.Date.now(),
                startValue: currentPosition.x,
                endValue  : x
            });

            easingY = this.getSnapEasing().y;
            easingY.setConfig({
                startTime : Ext.Date.now(),
                startValue: currentPosition.y,
                endValue  : y
            });

            this.activeEasing.x = easingX;
            this.activeEasing.y = easingY;

            this.startAnimation();

            return this;
        },

        /**
         * Scrolls to the end of the scrollable view
         * @return {Ext.scroll.Scroller} this
         */
        scrollToEnd: function() {
            this.scrollTo(0, this.getSize().y - this.getContainerSize().y);
            return this;
        },

        /**
         * Scrolls to the end of the scrollable view, animated.
         * @return {Ext.scroll.Scroller} this
         */
        scrollToEndAnimated: function() {
            this.scrollToAnimated(0, this.getSize().y - this.getContainerSize().y);
            return this;
        },

        /**
         * Attempts to snap to a slot, specified by an index. The is calculated using the {@link #snap}
         * configuration; and if no {@link #snap} value is set it will not work.
         *
         * So lets say you scroller has a {@link #snap} value of 50. If you call this method with the index
         * of `10`, it will scroll on the Y axis to `500px`.
         *
         * @param {Number} index The index of the slot you want to scroll too
         * @param {String} axis The axis you want to scroll. `x` or `y`.
         * @return {Ext.scroll.Scroller/Boolean} this
         */
        snapToSlot: function(index, axis) {
            var snap = this.getSnap(),
                offset = this.getSnapOffset()[axis],
                containerSize = this.getContainerSize(),
                size = this.getSize();

            if (!snap) {
                return false;
            }

            this.scrollToAnimated(0, Math.min(this.getSnap() * index, size.y - containerSize.y));

            return this;
        },

        /**
         * Change the scroll offset by the given amount
         * @param {Number} x The offset to scroll by on the x axis
         * @param {Number} y The offset to scroll by on the y axis
         * @return {Ext.util.Scroller} this
         */
        scrollBy: function(x, y) {
            var position = this.position;
            return this.scrollTo(position.x + offset.x, position.y + offset.y);
        },

        /**
         * Change the scroll offset by the given amount with animation
         * @param {Number} x The offset to scroll by on the x axis
         * @param {Number} y The offset to scroll by on the y axis
         * @return {Ext.util.Scroller} this
         */
        scrollByAnimated: function(x, y) {
            var position = this.position;
            return this.scrollToAnimated(position.x + offset.x, position.y + offset.y);
        },

        /**
         * @private
         */
        doScrollTo: function(x, y) {
            var containerDom = this.getContainer().dom;

            if (x !== null) {
                containerDom.scrollLeft = x;
            }

            if (y !== null) {
                containerDom.scrollTop = y;
            }
        },

        /**
         * @private
         */
        onTouchStart: function() {
            this.stopAnimation();
        },

        /**
         * @private
         */
        onDragStart: function(e) {
            var direction = this.getDirection(),
                absDeltaX = e.absDeltaX,
                absDeltaY = e.absDeltaY,
                directionLock = this.getDirectionLock(),
                startPosition = this.startPosition,
                flickStartPosition = this.flickStartPosition,
                flickStartTime = this.flickStartTime,
                lastDragPosition = this.lastDragPosition,
                currentPosition = this.position,
                dragDirection = this.dragDirection,
                x = currentPosition.x,
                y = currentPosition.y,
                now = Ext.Date.now();

            this.isDragging = true;

            if (directionLock && direction !== 'both') {
                if ((direction === 'horizontal' && absDeltaX > absDeltaY)
                        || (direction === 'vertical' && absDeltaY > absDeltaX)) {
                    e.stopPropagation();
                }
                else {
                    this.isDragging = false;
                    return;
                }
            }

            this.stopAnimation(true);

            lastDragPosition.x = x;
            lastDragPosition.y = y;

            flickStartPosition.x = x;
            flickStartPosition.y = y;

            startPosition.x = x;
            startPosition.y = y;

            flickStartTime.x = now;
            flickStartTime.y = now;

            dragDirection.x = 0;
            dragDirection.y = 0;

            this.dragStartTime = now;

            this.isDragging = true;

            this.fireEvent('scrollstart', this);
        },

        /**
         * @private
         */
        onAxisDrag: function(axis, delta) {
            if (!this.isAxisEnabled(axis)) {
                return;
            }

            var flickStartPosition = this.flickStartPosition,
                flickStartTime = this.flickStartTime,
                lastDragPosition = this.lastDragPosition,
                dragDirection = this.dragDirection,
                old = this.position[axis],
                min = this.getMinPosition()[axis],
                max = this.getMaxPosition()[axis],
                start = this.startPosition[axis],
                last = lastDragPosition[axis],
                current = start - delta,
                lastDirection = dragDirection[axis],
                restrictFactor = this.getOutOfBoundRestrictFactor(),
                startMomentumResetTime = this.getStartMomentumResetTime(),
                now = Ext.Date.now(),
                distance;

            if (current < min) {
                current *= restrictFactor;
            }
            else if (current > max) {
                distance = current - max;
                current = max + distance * restrictFactor;
            }

            if (current > last) {
                dragDirection[axis] = 1;
            }
            else if (current < last) {
                dragDirection[axis] = -1;
            }

            if ((lastDirection !== 0 && (dragDirection[axis] !== lastDirection))
                    || (now - flickStartTime[axis]) > startMomentumResetTime) {
                flickStartPosition[axis] = old;
                flickStartTime[axis] = now;
            }

            lastDragPosition[axis] = current;
        },

        /**
         * @private
         */
        onDrag: function(e) {
            if (!this.isDragging) {
                return;
            }

            var lastDragPosition = this.lastDragPosition;

            this.onAxisDrag('x', e.deltaX);
            this.onAxisDrag('y', e.deltaY);

            this.scrollTo(lastDragPosition.x, lastDragPosition.y);
        },

        /**
         * @private
         */
        onDragEnd: function(e) {
            var animationX, animationY;

            if (!this.isDragging) {
                return;
            }

            this.dragEndTime = Ext.Date.now();

            this.onDrag(e);

            this.isDragging = false;

            animationX = this.prepareAnimation('x');
            animationY = this.prepareAnimation('y');

            if (!(animationX === false || animationY === false)) {
                this.isScrolling = true;
            }

            this.startAnimation();
        },

        /**
         * @private
         */
        prepareAnimation: function(axis) {
            if (!this.isAxisEnabled(axis)) {
                return this;
            }

            var currentPosition = this.position[axis],
                flickStartPosition = this.flickStartPosition[axis],
                flickStartTime = this.flickStartTime[axis],
                minPosition = this.getMinPosition()[axis],
                maxPosition = this.getMaxPosition()[axis],
                maxAbsVelocity = this.getMaxAbsoluteVelocity(),
                boundValue = null,
                easing, velocity, duration;

            if (currentPosition < minPosition) {
                boundValue = minPosition;
            }
            else if (currentPosition > maxPosition) {
                boundValue = maxPosition;
            }

            // Out of bound, to be pulled back
            if (boundValue !== null) {
                easing = this.getSnapEasing()[axis];
                easing.setConfig({
                    startTime: this.dragEndTime,
                    startValue: currentPosition,
                    endValue: boundValue
                });
            }
            // Still within boundary, start deceleration
            else {
                duration = this.dragEndTime - flickStartTime;

                if (duration === 0) {
                    return false;
                }

                velocity = (currentPosition - flickStartPosition) / (this.dragEndTime - flickStartTime);

                if (velocity === 0) {
                    return;
                }

                if (velocity < -maxAbsVelocity) {
                    velocity = -maxAbsVelocity;
                }
                else if (velocity > maxAbsVelocity) {
                    velocity = maxAbsVelocity;
                }

                easing = this.getMomentumEasing()[axis];
                easing.setConfig({
                    startTime: this.dragEndTime,
                    startValue: currentPosition,
                    startVelocity: velocity,
                    minMomentumValue: 0,
                    maxMomentumValue: maxPosition
                });
            }

            this.activeEasing[axis] = easing;

            return this;
        },

        /**
         * @private
         */
        prepareSnapAnimation: function(axis) {
            if (!this.isAxisEnabled(axis)) {
                return false;
            }

            var currentPosition = this.position[axis],
                containerSize = this.getContainerSize()[axis],
                containerScrollSize = this.getContainerScrollSize()[axis],
                snap = this.getSnap(),
                offset = this.getSnapOffset()[axis],
                easing, endValue;

            endValue = Math.round((currentPosition + offset) / snap) * snap;

            //if the currentPosition is less than the containerScrollSize - containerSize (so it is at the end of the list),
            // then use it
            if ((containerScrollSize - containerSize) <= currentPosition) {
                return false;
            }

            easing = this.getSnapEasing()[axis];
            easing.setConfig({
                startTime : Ext.Date.now(),
                startValue: currentPosition,
                endValue  : endValue - offset
            });

            this.activeEasing[axis] = easing;

            return endValue;
        },

        /**
         * @private
         */
        startAnimation: function() {
            this.isAnimating = true;
            this.animationTimer = setInterval(this.doAnimationFrame, this.animationInterval);
            this.doAnimationFrame();
        },

        /**
         * @private
         */
        doAnimationFrame: function() {
            if (!this.isAnimating) {
                return;
            }

            var easing = this.activeEasing,
                easingX = easing.x,
                easingY = easing.y,
                isEasingXEnded = easingX === null || easingX.isEnded,
                isEasingYEnded = easingY === null || easingY.isEnded,
                x = null,
                y = null;

            if (isEasingXEnded && isEasingYEnded) {
                this.stopAnimation();
                return;
            }

            if (!isEasingXEnded) {
                x = easingX.getValue();
            }

            if (!isEasingYEnded) {
                y = easingY.getValue();
            }

            this.scrollTo(x, y);
        },

        /**
         * @private
         * Stops the animation of the scroller at any time.
         */
        stopAnimation: function(isOnTouchStart) {
            if (!this.isAnimating) {
                return;
            }

            var activeEasing = this.activeEasing;

            activeEasing.x = null;
            activeEasing.y = null;

            this.isAnimating = false;

            clearInterval(this.animationTimer);

            this.snapToBoundary();

            if (!isOnTouchStart) {
                if (this.onScrollEnd()) {
                    this.fireEvent('scrollend', this, this.position);
                }
            }

            this.isScrolling = false;
        },

        /**
         * @private
         */
        onScrollEnd: function() {
            if (this.isSnapping) {
                this.isSnapping = false;
                return true;
            }

            // Check the current position and calculate the snapping position
            var snap = this.getSnap(),
                snapX, snapY;

            if (!snap) {
                return true;
            }

            snapX = this.prepareSnapAnimation('x');
            snapY = this.prepareSnapAnimation('y');

            if (Ext.isNumber(snapX) || Ext.isNumber(snapY)) {
                this.isSnapping = true;

                this.startAnimation();

                return false;
            }

            return true;
        },

        /**
         * @private
         * Returns the snapped value of the specified valuea and axis.
         */
        snapValueForAxis: function(value, axis) {
            var snap = this.getSnap(),
                offset = this.getSnapOffset()[axis];

            value = Math.round((value + offset) / snap) * snap;

            return value;
        },

        /**
         * @private
         */
        snapToBoundary: function() {
            var position = this.position,
                minPosition = this.getMinPosition(),
                maxPosition = this.getMaxPosition(),
                minX = minPosition.x,
                minY = minPosition.y,
                maxX = maxPosition.x,
                maxY = maxPosition.y,
                x = position.x,
                y = position.y;

            if (x < minX) {
                x = minX;
            }
            else if (x > maxX) {
                x = maxX;
            }

            if (y < minY) {
                y = minY;
            }
            else if (y > maxY) {
                y = maxY;
            }

            this.scrollTo(x, y);
        },

        destroy: function() {
            var element = this.getElement(),
                sizeMonitors = this.sizeMonitors;

            if (sizeMonitors) {
                sizeMonitors.element.destroy();
                sizeMonitors.container.destroy();
            }

            if (element && !element.isDestroyed) {
                element.removeCls(this.getCls());
                this.getContainer().removeCls(this.getContainerCls());
            }

            this.callParent(arguments);
        }
    }, function() {
        //<deprecated product=touch since=2.0>

        this.override({
            constructor: function(config) {
                var acceleration, friction, springTension, minVelocity;

                /**
                 * @cfg {Number} acceleration A higher acceleration gives the scroller more initial velocity.
                 * @deprecated 2.0.0 Please use {@link #momentumEasing}.momentum.acceleration and {@link #momentumEasing}.bounce.acceleration instead.
                 */
                if (config.hasOwnProperty('acceleration')) {
                    acceleration = config.acceleration;
                    delete config.acceleration;
                    //<debug warn>
                    Ext.Logger.deprecate("'acceleration' config is deprecated, set momentumEasing.momentum.acceleration and momentumEasing.bounce.acceleration configs instead");
                    //</debug>

                    Ext.merge(config, {
                        momentumEasing: {
                            momentum: { acceleration: acceleration },
                            bounce: { acceleration: acceleration }
                        }
                    });
                }

                /**
                 * @cfg {Number} friction The friction of the scroller. By raising this value the length that momentum scrolls
                 * becomes shorter. This value is best kept between 0 and 1.
                 * @deprecated 2.0.0 Please set the {@link #momentumEasing}.momentum.friction configuration instead
                 */
                if (config.hasOwnProperty('friction')) {
                    friction = config.friction;
                    delete config.friction;
                    //<debug warn>
                    Ext.Logger.deprecate("'friction' config is deprecated, set momentumEasing.momentum.friction config instead");
                    //</debug>

                    Ext.merge(config, {
                        momentumEasing: {
                            momentum: { friction: friction }
                        }
                    });
                }

                if (config.hasOwnProperty('springTension')) {
                    springTension = config.springTension;
                    delete config.springTension;
                    //<debug warn>
                    Ext.Logger.deprecate("'springTension' config is deprecated, set momentumEasing.momentum.springTension config instead");
                    //</debug>

                    Ext.merge(config, {
                        momentumEasing: {
                            momentum: { springTension: springTension }
                        }
                    });
                }

                if (config.hasOwnProperty('minVelocityForAnimation')) {
                    minVelocity = config.minVelocityForAnimation;
                    delete config.minVelocityForAnimation;
                    //<debug warn>
                    Ext.Logger.deprecate("'minVelocityForAnimation' config is deprecated, set momentumEasing.minVelocity config instead");
                    //</debug>

                    Ext.merge(config, {
                        momentumEasing: {
                            minVelocity: minVelocity
                        }
                    });
                }

                this.callOverridden(arguments);
            },

            /**
             * Updates the boundary information for this scroller.
             * @return {Ext.scroll.Scroller} this
             * @deprecated 2.0.0 Please use {@link #method-refresh} instead.
             */
            updateBoundary: function() {
                //<debug warn>
                Ext.Logger.deprecate("updateBoundary() is deprecated, use refresh() instead");
                //</debug>
                return this.refresh();
            },

            //we need to support the old way of specifying an object and boolean, so we override the original function
            scrollBy: function(offset, animate) {
                var position = this.position,
                    scrollTo;

                //check if offset is not an number
                if (!Ext.isNumber(offset)) {
                    //<debug warn>
                    Ext.Logger.deprecate("calling scrollBy with an object is no longer supporter. Please pass both the x and y values.");
                    //</debug>

                    //ensure it is a boolean
                    animate = Boolean(animate);

                    scrollTo = {
                        x: position.x + offset.x,
                        y: position.y + offset.y
                    };
                } else {
                    animate = false;

                    scrollTo = {
                        x: position.x + offset,
                        y: position.y + animate
                    };
                }

                if (animate) {
                    this.scrollToAnimated(scrollTo.x, scrollTo.y);
                } else {
                    this.scrollTo(scrollTo.x, scrollTo.y);
                }

                return this;
            },

            /**
             * Sets the offset of this scroller.
             * @param {Object} offset The offset to move to
             * @param {Number} offset.x The x axis offset
             * @param {Number} offset.y The y axis offset
             * @deprecated 2.0.0 Please use {@link #scrollTo} instead
             * @return {Ext.scroll.Scroller} this
             */
            setOffset: function(offset) {
                return this.scrollToAnimated(-offset.x, -offset.y);
            }
        });
        //</deprecated>
    });