    /**
     * @class Ext.grid.Scroller
     * @extends Ext.Component
     *
     * Docked in an Ext.grid.Panel, controls virtualized scrolling and synchronization
     * across different sections.
     *
     * @private
     */
    Ext.define('Ext.grid.Scroller', {
        extend: 'Ext.Component',
        alias: 'widget.gridscroller',
        weight: 110,
        cls: Ext.baseCSSPrefix + 'scroller',
        focusable: false,

        renderTpl: ['<div class="' + Ext.baseCSSPrefix + 'stretcher"></div>'],

        initComponent: function() {
            var me       = this,
                dock     = me.dock,
                cls      = Ext.baseCSSPrefix + 'scroller-vertical',
                sizeProp = 'width',
                // Subtracting 2px would give us a perfect fit of the scroller
                // however, some browsers wont allow us to scroll content thats not
                // visible, therefore we use 1px.
                // Note: This 1px offset matches code in Ext.grid.ColumnLayout when
                // reserving room for the scrollbar
                scrollbarWidth = Ext.getScrollBarWidth() + (Ext.isIE ? 1 : -1);

            me.offsets = {bottom: 0};

            if (dock === 'top' || dock === 'bottom') {
                cls = Ext.baseCSSPrefix + 'scroller-horizontal';
                sizeProp = 'height';
            }
            me[sizeProp] = scrollbarWidth;

            me.cls += (' ' + cls);

            Ext.applyIf(me.renderSelectors, {
                stretchEl: '.' + Ext.baseCSSPrefix + 'stretcher'
            });
            me.callParent();
        },

        afterRender: function() {
            var me = this;
            me.callParent();
            me.ownerCt.on('afterlayout', me.onOwnerAfterLayout, me);
            me.mon(me.el, 'scroll', me.onElScroll, me);
            Ext.cache[me.el.id].skipGarbageCollection = true;
        },

        getSizeCalculation: function() {
            var owner  = this.getPanel(),
                dock   = this.dock,
                elDom  = this.el.dom,
                width  = 1,
                height = 1,
                view, tbl;

            if (dock === 'top' || dock === 'bottom') {
                // TODO: Must gravitate to a single region..
                // Horizontal scrolling only scrolls virtualized region
                var items  = owner.query('tableview'),
                    center = items[1] || items[0];

                if (!center) {
                    return false;
                }
                // center is not guaranteed to have content, such as when there
                // are zero rows in the grid/tree. We read the width from the
                // headerCt instead.
                width = center.headerCt.getFullWidth();

                if (Ext.isIEQuirks) {
                    width--;
                }
                // Account for the 1px removed in Scroller.
                width--;
            } else {
                view = owner.down('tableview:not([lockableInjected])');
                if (!view) {
                    return false;
                }
                tbl = view.el;
                if (!tbl) {
                    return false;
                }

                // needs to also account for header and scroller (if still in picture)
                // should calculate from headerCt.
                height = tbl.dom.scrollHeight;
            }
            if (isNaN(width)) {
                width = 1;
            }
            if (isNaN(height)) {
                height = 1;
            }
            return {
                width: width,
                height: height
            };
        },

        invalidate: function(firstPass) {
            if (!this.stretchEl || !this.ownerCt) {
                return;
            }
            var size  = this.getSizeCalculation(),
                elDom = this.el.dom;
            if (size) {
                this.stretchEl.setSize(size);

                // BrowserBug: IE7
                // This makes the scroller enabled, when initially rendering.
                elDom.scrollTop = elDom.scrollTop;
            }
        },

        onOwnerAfterLayout: function(owner, layout) {
            this.invalidate();
        },

        /**
         * Sets the scrollTop and constrains the value between 0 and max.
         * @param {Number} scrollTop
         * @return {Number} The resulting scrollTop value after being constrained
         */
        setScrollTop: function(scrollTop) {
            if (this.el) {
                var elDom = this.el.dom;
                return elDom.scrollTop = Ext.Number.constrain(scrollTop, 0, elDom.scrollHeight - elDom.clientHeight);
            }
        },

        /**
         * Sets the scrollLeft and constrains the value between 0 and max.
         * @param {Number} scrollLeft
         * @return {Number} The resulting scrollLeft value after being constrained
         */
        setScrollLeft: function(scrollLeft) {
            if (this.el) {
                var elDom = this.el.dom;
                return elDom.scrollLeft = Ext.Number.constrain(scrollLeft, 0, elDom.scrollWidth - elDom.clientWidth);
            }
        },

        /**
         * Scroll by deltaY
         * @param {Number} delta
         * @return {Number} The resulting scrollTop value
         */
        scrollByDeltaY: function(delta) {
            if (this.el) {
                var elDom = this.el.dom;
                return this.setScrollTop(elDom.scrollTop + delta);
            }
        },

        /**
         * Scroll by deltaX
         * @param {Number} delta
         * @return {Number} The resulting scrollLeft value
         */
        scrollByDeltaX: function(delta) {
            if (this.el) {
                var elDom = this.el.dom;
                return this.setScrollLeft(elDom.scrollLeft + delta);
            }
        },

        /**
         * Scroll to the top.
         */
        scrollToTop : function(){
            this.setScrollTop(0);
        },

        // synchronize the scroller with the bound gridviews
        onElScroll: function(event, target) {
            this.fireEvent('bodyscroll', event, target);
        },

        getPanel: function() {
            var me = this;
            if (!me.panel) {
                me.panel = this.up('[scrollerOwner]');
            }
            return me.panel;
        }
    });