    /**
     * This plugin adds pull to refresh functionality to the List.
     *
     * ## Example
     *
     *     Ext.define('TweetList', {
     *         extend: 'Ext.List',
     *
     *         config: {
     *             store: Ext.create('TweetStore'),
     *
     *             plugins: [
     *                 {
     *                     xclass: 'Ext.plugin.PullRefresh',
     *                     pullRefreshText: 'Pull down for more new Tweets!'
     *                 }
     *             ],
     *
     *             itemTpl: [
     *                 '<img src="{profile_image_url}" />',
     *                 '<div class="tweet">{text}</div>'
     *             ]
     *         }
     *     });
     */
    Ext.define('Ext.plugin.PullRefresh', {
        extend: 'Ext.Component',
        alias: 'plugin.pullrefresh',
        requires: ['Ext.DateExtras'],

        config: {
            /*
             * @accessor
             */
            list: null,

            /*
             * @cfg {String} pullRefreshText The text that will be shown while you are pulling down.
             * @accessor
             */
            pullRefreshText: 'Pull down to refresh...',

            /*
             * @cfg {String} releaseRefreshText The text that will be shown after you have pulled down enough to show the release message.
             * @accessor
             */
            releaseRefreshText: 'Release to refresh...',

            /*
             * @cfg {String} loadingText The text that will be shown while the list is refreshing.
             * @accessor
             */
            loadingText: 'Loading...',

            /*
             * @cfg {Number} snappingAnimationDuration The duration for snapping back animation after the data has been refreshed
             * @accessor
             */
            snappingAnimationDuration: 150,

            /*
             * @cfg {Function} refreshFn The function that will be called to refresh the list.
             * If this is not defined, the store's load function will be called.
             * The refresh function gets called with a reference to this plugin instance.
             * @accessor
             */
            refreshFn: null,

            /*
             * @cfg {XTemplate/String/Array} pullTpl The template being used for the pull to refresh markup.
             * @accessor
             */
            pullTpl: [
                '<div class="x-list-pullrefresh">',
                    '<div class="x-list-pullrefresh-arrow"></div>',
                    '<div class="x-loading-spinner">',
                        '<span class="x-loading-top"></span>',
                        '<span class="x-loading-right"></span>',
                        '<span class="x-loading-bottom"></span>',
                        '<span class="x-loading-left"></span>',
                    '</div>',
                    '<div class="x-list-pullrefresh-wrap">',
                        '<h3 class="x-list-pullrefresh-message">{message}</h3>',
                        '<div class="x-list-pullrefresh-updated">Last Updated: <span>{lastUpdated:date("m/d/Y h:iA")}</span></div>',
                    '</div>',
                '</div>'
            ].join('')
        },

        isRefreshing: false,
        currentViewState: '',

        initialize: function() {
            this.callParent();

            this.on({
                painted: 'onPainted',
                scope: this
            });
        },

        onPainted: function() {
            this.pullHeight = this.loadingElement.getHeight();
        },

        init: function(list) {
            var me = this,
                pullTpl = me.getPullTpl(),
                element = me.element,
                scroller = list.getScrollable().getScroller();

            me.setList(list);
            me.lastUpdated = new Date();

            list.insert(0, me);

            // Disable main list load mask
            list.setLoadingText(null);

            pullTpl.overwrite(element, {
                message: me.getPullRefreshText(),
                lastUpdated: me.lastUpdated
            }, true);

            me.loadingElement = element.getFirstChild();
            me.messageEl = element.down('.x-list-pullrefresh-message');
            me.updatedEl = element.down('.x-list-pullrefresh-updated > span');

            me.maxScroller = scroller.getMaxPosition();

            scroller.on({
                maxpositionchange: me.setMaxScroller,
                scroll: me.onScrollChange,
                scope: me
            });
        },

        setMaxScroller: function(scroller, position) {
            this.maxScroller = position;
        },

        onScrollChange: function(scroller, x, y) {
            if (y < 0) {
                this.onBounceTop(y);
            }
            if (y > this.maxScroller.y) {
                this.onBounceBottom(y);
            }
        },

        /**
         * @private
         */
        applyPullTpl: function(config) {
            return (Ext.isObject(config) && config.isTemplate) ? config : new Ext.XTemplate(config);
        },

        onBounceTop: function(y) {
            var me = this,
                list = me.getList(),
                scroller = list.getScrollable().getScroller();

            if (!me.isRefreshing && -y >= me.pullHeight + 10) {
                me.isRefreshing = true;

                me.setViewState('release');

                scroller.getContainer().onBefore({
                    dragend: 'onScrollerDragEnd',
                    single: true,
                    scope: me
                });
            }
            else if (me.isRefreshing && -y < me.pullHeight + 10) {
                me.isRefreshing = false;
                me.setViewState('pull');
            }
        },

        onScrollerDragEnd: function() {
            var me = this;
            if (me.isRefreshing) {
                var list = me.getList(),
                    scroller = list.getScrollable().getScroller();

                scroller.minPosition.y = -me.pullHeight;
                scroller.on({
                    scrollend: 'loadStore',
                    single: true,
                    scope: me
                });
            }
        },

        loadStore: function() {
            var me = this,
                list = me.getList(),
                scroller = list.getScrollable().getScroller(),
                store = list.getStore();

            me.setViewState('loading');
            Ext.defer(function() {
                scroller.on({
                    scrollend: function() {
                        if (me.getRefreshFn()) {
                            me.getRefreshFn().call(me, me);
                        } else {
                            store.load();
                        }
                        me.resetRefreshState();
                    },
                    delay: 100,
                    single: true,
                    scope: me
                });
                scroller.minPosition.y = 0;
                scroller.scrollToAnimated(null, 0);
            }, 500, me);
        },

        onBounceBottom: Ext.emptyFn,

        setViewState: function(state) {
            var me = this;
            var prefix = Ext.baseCSSPrefix,
                messageEl = me.messageEl,
                loadingElement = me.loadingElement;

            if (state === me.currentViewState) {
                return me;
            }
            me.currentViewState = state;

            if (messageEl && loadingElement) {
                switch (state) {
                    case 'pull':
                        messageEl.setHtml(me.getPullRefreshText());
                        loadingElement.removeCls([prefix + 'list-pullrefresh-release', prefix + 'list-pullrefresh-loading']);
                    break;

                    case 'release':
                        messageEl.setHtml(me.getReleaseRefreshText());
                        loadingElement.addCls(prefix + 'list-pullrefresh-release');
                    break;

                    case 'loading':
                        messageEl.setHtml(me.getLoadingText());
                        loadingElement.addCls(prefix + 'list-pullrefresh-loading');
                        break;
                }
            }

            return me;
        },

        resetRefreshState: function() {
            var me = this;
            me.isRefreshing = false;
            me.lastUpdated = new Date();

            me.setViewState('pull');
            me.updatedEl.setHtml(Ext.util.Format.date(me.lastUpdated, "m/d/Y h:iA"));
        }
    });