    /**
     * @class Ext.util.Draggable
     *
     * A core util class to bring Draggable behavior to any DOM element
     */

    Ext.define('Ext.util.Draggable', {
        isDraggable: true,

        mixins: [
            'Ext.mixin.Observable'
        ],

        requires: [
            'Ext.util.SizeMonitor',
            'Ext.util.Translatable'
        ],

        /**
         * @event dragstart
         * @preventable initDragStart
         * Fires whenever the component starts to be dragged
         * @param {Ext.util.Draggable} this
         * @param {Ext.EventObject} e the event object
         * @param {Object} offset The offset object
         */

        /**
         * @event drag
         * Fires whenever the component is dragged
         * @param {Ext.util.Draggable} this
         * @param {Ext.EventObject} e the event object
         * @param {Object} offset The offset object
         */

        /**
         * @event dragend
         * Fires whenever the component is dragged
         * @param {Ext.util.Draggable} this
         * @param {Ext.EventObject} e the event object
         */

        config: {
            cls: Ext.baseCSSPrefix + 'draggable',

            draggingCls: Ext.baseCSSPrefix + 'dragging',

            element: null,

            constraint: 'container',

            disabled: null,

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

            translatable: {}
        },

        DIRECTION_BOTH: 'both',

        DIRECTION_VERTICAL: 'vertical',

        DIRECTION_HORIZONTAL: 'horizontal',

        /**
         * Creates new Draggable.
         * @param {Mixed} el The element you want to make draggable.
         * @param {Object} config The configuration object for this Draggable.
         */
        constructor: function(config) {
            var element;

            this.sizeMonitors = {};

            this.extraConstraint = {};

            this.initialConfig = config;

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

            this.listeners = {
                dragstart: 'onDragStart',
                drag     : 'onDrag',
                dragend  : 'onDragEnd',

                scope: this
            };

            if (config && config.element) {
                element = config.element;
                delete config.element;

                this.setElement(element);
            }

            return this;
        },

        applyElement: function(element) {
            if (!element) {
                return;
            }

            return Ext.get(element);
        },

        updateElement: function(element) {
            element.on(this.listeners);
            element.addCls(this.getCls() || this.config.cls);

            this.sizeMonitors.element = new Ext.util.SizeMonitor({
                element: element,
                callback: this.doRefresh,
                scope: this
            });

            this.initConfig(this.initialConfig);
        },

        applyTranslatable: function(translatable, currentInstance) {
            translatable = Ext.factory(translatable, Ext.util.Translatable, currentInstance);
            translatable.setElement(this.getElement());

            return translatable;
        },

        setExtraConstraint: function(constraint) {
            this.extraConstraint = constraint || {};

            this.refreshConstraint();

            return this;
        },

        addExtraConstraint: function(constraint) {
            Ext.merge(this.extraConstraint, constraint);

            this.refreshConstraint();

            return this;
        },

        applyConstraint: function(newConstraint, currentConstraint) {
            this.currentConstraint = newConstraint;

            if (newConstraint === 'container') {
                return Ext.merge(this.getContainerConstraint(), this.extraConstraint);
            }

            return Ext.merge({}, this.extraConstraint, newConstraint);
        },

        updateConstraint: function() {
            this.refreshOffset();
        },

        getContainerConstraint: function() {
            var container = this.getContainer();

            if (!container) {
                return {
                    min: { x: -Infinity, y: -Infinity },
                    max: { x: Infinity, y: Infinity }
                };
            }

            var dom = this.getElement().dom,
                containerDom = container.dom,
                width = dom.offsetWidth,
                height = dom.offsetHeight,
                containerWidth = containerDom.offsetWidth,
                containerHeight = containerDom.offsetHeight;

            return {
                min: { x: 0, y: 0 },
                max: { x: containerWidth - width, y: containerHeight - height }
            };
        },

        getContainer: function() {
            var container = this.container;

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

                if (container) {
                    this.sizeMonitors.container = new Ext.util.SizeMonitor({
                        element: container,
                        callback: this.doRefresh,
                        scope: this
                    });

                    this.container = container;
                }
            }

            return container;
        },

        detachListeners: function() {
            this.getElement().un(this.listeners);
        },

        isAxisEnabled: function(axis) {
            var direction = this.getDirection();

            if (axis === 'x') {
                return (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_HORIZONTAL);
            }

            return (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_VERTICAL);
        },

        onDragStart: function(e) {
            if (this.getDisabled()) {
                return false;
            }

            this.fireAction('dragstart', [this, e, this.offset], this.initDragStart);
        },

        initDragStart: function(me, e, startOffset) {
            this.dragStartOffset = {
                x: startOffset.x,
                y: startOffset.y
            };

            this.isDragging = true;

            this.getElement().addCls(this.getDraggingCls());
        },

        onDrag: function(e) {
            if (!this.isDragging) {
                return;
            }

            var startOffset = this.dragStartOffset;

            this.fireAction('drag', [this, e, {
                x: startOffset.x + e.deltaX,
                y: startOffset.y + e.deltaY
            }], this.doDrag);
        },

        doDrag: function(me, e, offset) {
            me.setOffset(offset);
        },

        onDragEnd: function(e) {
            if (!this.isDragging) {
                return;
            }

            this.onDrag(e);

            this.isDragging = false;

            this.fireEvent('dragend', this, e);

            this.getElement().removeCls(this.getDraggingCls());
        },

        setOffset: function(offset, animation) {
            var currentOffset = this.offset,
                x = offset.x,
                y = offset.y,
                constraint = this.getConstraint(),
                minOffset = constraint.min,
                maxOffset = constraint.max,
                min = Math.min,
                max = Math.max;

            if (this.isAxisEnabled('x') && typeof x == 'number') {
                x = min(max(x, minOffset.x), maxOffset.x);
            }
            else {
                x = currentOffset.x;
            }

            if (this.isAxisEnabled('y') && typeof y == 'number') {
                y = min(max(y, minOffset.y), maxOffset.y);
            }
            else {
                y = currentOffset.y;
            }

            currentOffset.x = x;
            currentOffset.y = y;

            this.getTranslatable().translate(currentOffset, animation);
        },

        getOffset: function() {
            return this.offset;
        },

        refreshConstraint: function() {
            this.setConstraint(this.currentConstraint);
        },

        refreshOffset: function() {
            this.setOffset(this.offset);
        },

        doRefresh: function() {
            this.refreshConstraint();
            this.getTranslatable().refresh();
            this.refreshOffset();
        },

        refresh: function() {
            var sizeMonitors = this.sizeMonitors;

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

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

            this.doRefresh();
        },

        /**
         * Enable the Draggable.
         * @return {Ext.util.Draggable} This Draggable instance
         */
        enable: function() {
            return this.setDisabled(false);
        },

        /**
         * Disable the Draggable.
         * @return {Ext.util.Draggable} This Draggable instance
         */
        disable: function() {
            return this.setDisabled(true);
        },

        destroy: function() {
            var sizeMonitors = this.sizeMonitors,
                translatable = this.getTranslatable();

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

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

            var element = this.getElement();
            if (element && !element.isDestroyed) {
                element.removeCls(this.getCls());
            }
            this.detachListeners();

            if (translatable) {
                translatable.destroy();
            }
        }

    }, function() {
        //<deprecated product=touch since=2.0>
        this.override({
            constructor: function(config) {
                if (config && config.constrain) {
                    //<debug warn>
                    Ext.Logger.deprecate("'constrain' config is deprecated, please use 'contraint' instead");
                    //</debug>
                    config.contraint = config.constrain;
                    delete config.constrain;
                }

                return this.callOverridden(arguments);
            }
        });
        //</deprecated>
    });