    /*!
     * Ext JS Library 3.4.0
     * Copyright(c) 2006-2011 Sencha Inc.
     * licensing@sencha.com
     * http://www.sencha.com/license
     */
    /**
     * @class Ext.tree.TreeDropZone
     * @extends Ext.dd.DropZone
     * @constructor
     * @param {String/HTMLElement/Element} tree The {@link Ext.tree.TreePanel} for which to enable dropping
     * @param {Object} config
     */
    if(Ext.dd.DropZone){

    Ext.tree.TreeDropZone = function(tree, config){
        /**
         * @cfg {Boolean} allowParentInsert
         * Allow inserting a dragged node between an expanded parent node and its first child that will become a
         * sibling of the parent when dropped (defaults to false)
         */
        this.allowParentInsert = config.allowParentInsert || false;
        /**
         * @cfg {String} allowContainerDrop
         * True if drops on the tree container (outside of a specific tree node) are allowed (defaults to false)
         */
        this.allowContainerDrop = config.allowContainerDrop || false;
        /**
         * @cfg {String} appendOnly
         * True if the tree should only allow append drops (use for trees which are sorted, defaults to false)
         */
        this.appendOnly = config.appendOnly || false;

        Ext.tree.TreeDropZone.superclass.constructor.call(this, tree.getTreeEl(), config);
        /**
        * The TreePanel for this drop zone
        * @type Ext.tree.TreePanel
        * @property
        */
        this.tree = tree;
        /**
        * Arbitrary data that can be associated with this tree and will be included in the event object that gets
        * passed to any nodedragover event handler (defaults to {})
        * @type Ext.tree.TreePanel
        * @property
        */
        this.dragOverData = {};
        // private
        this.lastInsertClass = "x-tree-no-status";
    };

    Ext.extend(Ext.tree.TreeDropZone, Ext.dd.DropZone, {
        /**
         * @cfg {String} ddGroup
         * A named drag drop group to which this object belongs.  If a group is specified, then this object will only
         * interact with other drag drop objects in the same group (defaults to 'TreeDD').
         */
        ddGroup : "TreeDD",

        /**
         * @cfg {String} expandDelay
         * The delay in milliseconds to wait before expanding a target tree node while dragging a droppable node
         * over the target (defaults to 1000)
         */
        expandDelay : 1000,

        // private
        expandNode : function(node){
            if(node.hasChildNodes() && !node.isExpanded()){
                node.expand(false, null, this.triggerCacheRefresh.createDelegate(this));
            }
        },

        // private
        queueExpand : function(node){
            this.expandProcId = this.expandNode.defer(this.expandDelay, this, [node]);
        },

        // private
        cancelExpand : function(){
            if(this.expandProcId){
                clearTimeout(this.expandProcId);
                this.expandProcId = false;
            }
        },

        // private
        isValidDropPoint : function(n, pt, dd, e, data){
            if(!n || !data){ return false; }
            var targetNode = n.node;
            var dropNode = data.node;
            // default drop rules
            if(!(targetNode && targetNode.isTarget && pt)){
                return false;
            }
            if(pt == "append" && targetNode.allowChildren === false){
                return false;
            }
            if((pt == "above" || pt == "below") && (targetNode.parentNode && targetNode.parentNode.allowChildren === false)){
                return false;
            }
            if(dropNode && (targetNode == dropNode || dropNode.contains(targetNode))){
                return false;
            }
            // reuse the object
            var overEvent = this.dragOverData;
            overEvent.tree = this.tree;
            overEvent.target = targetNode;
            overEvent.data = data;
            overEvent.point = pt;
            overEvent.source = dd;
            overEvent.rawEvent = e;
            overEvent.dropNode = dropNode;
            overEvent.cancel = false;
            var result = this.tree.fireEvent("nodedragover", overEvent);
            return overEvent.cancel === false && result !== false;
        },

        // private
        getDropPoint : function(e, n, dd){
            var tn = n.node;
            if(tn.isRoot){
                return tn.allowChildren !== false ? "append" : false; // always append for root
            }
            var dragEl = n.ddel;
            var t = Ext.lib.Dom.getY(dragEl), b = t + dragEl.offsetHeight;
            var y = Ext.lib.Event.getPageY(e);
            var noAppend = tn.allowChildren === false || tn.isLeaf();
            if(this.appendOnly || tn.parentNode.allowChildren === false){
                return noAppend ? false : "append";
            }
            var noBelow = false;
            if(!this.allowParentInsert){
                noBelow = tn.hasChildNodes() && tn.isExpanded();
            }
            var q = (b - t) / (noAppend ? 2 : 3);
            if(y >= t && y < (t + q)){
                return "above";
            }else if(!noBelow && (noAppend || y >= b-q && y <= b)){
                return "below";
            }else{
                return "append";
            }
        },

        // private
        onNodeEnter : function(n, dd, e, data){
            this.cancelExpand();
        },

        onContainerOver : function(dd, e, data) {
            if (this.allowContainerDrop && this.isValidDropPoint({ ddel: this.tree.getRootNode().ui.elNode, node: this.tree.getRootNode() }, "append", dd, e, data)) {
                return this.dropAllowed;
            }
            return this.dropNotAllowed;
        },

        // private
        onNodeOver : function(n, dd, e, data){
            var pt = this.getDropPoint(e, n, dd);
            var node = n.node;

            // auto node expand check
            if(!this.expandProcId && pt == "append" && node.hasChildNodes() && !n.node.isExpanded()){
                this.queueExpand(node);
            }else if(pt != "append"){
                this.cancelExpand();
            }

            // set the insert point style on the target node
            var returnCls = this.dropNotAllowed;
            if(this.isValidDropPoint(n, pt, dd, e, data)){
               if(pt){
                   var el = n.ddel;
                   var cls;
                   if(pt == "above"){
                       returnCls = n.node.isFirst() ? "x-tree-drop-ok-above" : "x-tree-drop-ok-between";
                       cls = "x-tree-drag-insert-above";
                   }else if(pt == "below"){
                       returnCls = n.node.isLast() ? "x-tree-drop-ok-below" : "x-tree-drop-ok-between";
                       cls = "x-tree-drag-insert-below";
                   }else{
                       returnCls = "x-tree-drop-ok-append";
                       cls = "x-tree-drag-append";
                   }
                   if(this.lastInsertClass != cls){
                       Ext.fly(el).replaceClass(this.lastInsertClass, cls);
                       this.lastInsertClass = cls;
                   }
               }
           }
           return returnCls;
        },

        // private
        onNodeOut : function(n, dd, e, data){
            this.cancelExpand();
            this.removeDropIndicators(n);
        },

        // private
        onNodeDrop : function(n, dd, e, data){
            var point = this.getDropPoint(e, n, dd);
            var targetNode = n.node;
            targetNode.ui.startDrop();
            if(!this.isValidDropPoint(n, point, dd, e, data)){
                targetNode.ui.endDrop();
                return false;
            }
            // first try to find the drop node
            var dropNode = data.node || (dd.getTreeNode ? dd.getTreeNode(data, targetNode, point, e) : null);
            return this.processDrop(targetNode, data, point, dd, e, dropNode);
        },

        onContainerDrop : function(dd, e, data){
            if (this.allowContainerDrop && this.isValidDropPoint({ ddel: this.tree.getRootNode().ui.elNode, node: this.tree.getRootNode() }, "append", dd, e, data)) {
                var targetNode = this.tree.getRootNode();
                targetNode.ui.startDrop();
                var dropNode = data.node || (dd.getTreeNode ? dd.getTreeNode(data, targetNode, 'append', e) : null);
                return this.processDrop(targetNode, data, 'append', dd, e, dropNode);
            }
            return false;
        },

        // private
        processDrop: function(target, data, point, dd, e, dropNode){
            var dropEvent = {
                tree : this.tree,
                target: target,
                data: data,
                point: point,
                source: dd,
                rawEvent: e,
                dropNode: dropNode,
                cancel: !dropNode,
                dropStatus: false
            };
            var retval = this.tree.fireEvent("beforenodedrop", dropEvent);
            if(retval === false || dropEvent.cancel === true || !dropEvent.dropNode){
                target.ui.endDrop();
                return dropEvent.dropStatus;
            }

            target = dropEvent.target;
            if(point == 'append' && !target.isExpanded()){
                target.expand(false, null, function(){
                    this.completeDrop(dropEvent);
                }.createDelegate(this));
            }else{
                this.completeDrop(dropEvent);
            }
            return true;
        },

        // private
        completeDrop : function(de){
            var ns = de.dropNode, p = de.point, t = de.target;
            if(!Ext.isArray(ns)){
                ns = [ns];
            }
            var n;
            for(var i = 0, len = ns.length; i < len; i++){
                n = ns[i];
                if(p == "above"){
                    t.parentNode.insertBefore(n, t);
                }else if(p == "below"){
                    t.parentNode.insertBefore(n, t.nextSibling);
                }else{
                    t.appendChild(n);
                }
            }
            n.ui.focus();
            if(Ext.enableFx && this.tree.hlDrop){
                n.ui.highlight();
            }
            t.ui.endDrop();
            this.tree.fireEvent("nodedrop", de);
        },

        // private
        afterNodeMoved : function(dd, data, e, targetNode, dropNode){
            if(Ext.enableFx && this.tree.hlDrop){
                dropNode.ui.focus();
                dropNode.ui.highlight();
            }
            this.tree.fireEvent("nodedrop", this.tree, targetNode, data, dd, e);
        },

        // private
        getTree : function(){
            return this.tree;
        },

        // private
        removeDropIndicators : function(n){
            if(n && n.ddel){
                var el = n.ddel;
                Ext.fly(el).removeClass([
                        "x-tree-drag-insert-above",
                        "x-tree-drag-insert-below",
                        "x-tree-drag-append"]);
                this.lastInsertClass = "_noclass";
            }
        },

        // private
        beforeDragDrop : function(target, e, id){
            this.cancelExpand();
            return true;
        },

        // private
        afterRepair : function(data){
            if(data && Ext.enableFx){
                data.node.ui.highlight();
            }
            this.hideProxy();
        }
    });

    }