    /**
     * @author Jacky Nguyen <jacky@sencha.com>
     * @private
     */
    Ext.define('Ext.fx.runner.CssTransition', {
        extend: 'Ext.fx.runner.Css',

        listenersAttached: false,

        constructor: function() {
            this.runningAnimationsData = {};

            return this.callParent(arguments);
        },

        attachListeners: function() {
            this.listenersAttached = true;
            this.getEventDispatcher().addListener('element', '*', 'transitionend', 'onTransitionEnd', this);
        },

        onTransitionEnd: function(e) {
            var target = e.target,
                id = target.id;

            if (id && this.runningAnimationsData.hasOwnProperty(id)) {
                this.refreshRunningAnimationsData(Ext.get(target), [e.browserEvent.propertyName]);
            }
        },

        onAnimationEnd: function(element, data, animation, isInterrupted, isReplaced) {
            var id = element.getId(),
                runningData = this.runningAnimationsData[id],
                runningNameMap = runningData.nameMap,
                endRules = {},
                endData = {},
                toPropertyNames, i, ln, name;

            endRules[id] = endData;

            if (data.onBeforeEnd) {
                data.onBeforeEnd.call(data.scope || this, element, isInterrupted);
            }

            animation.fireEvent('animationbeforeend', animation, element, isInterrupted);

            if (isReplaced || (!isInterrupted && !data.preserveEndState)) {
                toPropertyNames = data.toPropertyNames;

                for (i = 0,ln = toPropertyNames.length; i < ln; i++) {
                    name = toPropertyNames[i];

                    if (!runningNameMap.hasOwnProperty(name)) {
                        endData[name] = null;
                    }
                }
            }

            if (data.after) {
                Ext.merge(endData, data.after);
            }

            this.applyStyles(endRules);

            if (data.onEnd) {
                data.onEnd.call(data.scope || this, element, isInterrupted);
            }

            animation.fireEvent('animationend', animation, element, isInterrupted);
        },

        onAllAnimationsEnd: function(element) {
            var id = element.getId(),
                endRules = {};

            delete this.runningAnimationsData[id];

            endRules[id] = {
                'transition-property': null,
                'transition-duration': null,
                'transition-timing-function': null,
                'transition-delay': null
            };

            this.applyStyles(endRules);
        },

        hasRunningAnimations: function(element) {
            var id = element.getId(),
                runningAnimationsData = this.runningAnimationsData;

            return runningAnimationsData.hasOwnProperty(id) && runningAnimationsData[id].sessions.length > 0;
        },

        refreshRunningAnimationsData: function(element, propertyNames, interrupt, replace) {
            var id = element.getId(),
                runningAnimationsData = this.runningAnimationsData,
                runningData = runningAnimationsData[id],
                nameMap = runningData.nameMap,
                nameList = runningData.nameList,
                sessions = runningData.sessions,
                ln, j, subLn, name,
                i, session, map, list,
                hasCompletedSession = false;

            interrupt = Boolean(interrupt);
            replace = Boolean(replace);

            if (!sessions) {
                return this;
            }

            ln = sessions.length;

            if (ln === 0) {
                return this;
            }

            if (replace) {
                runningData.nameMap = {};
                nameList.length = 0;

                for (i = 0; i < ln; i++) {
                    session = sessions[i];
                    this.onAnimationEnd(element, session.data, session.animation, interrupt, replace);
                }

                sessions.length = 0;
            }
            else {
                for (i = 0; i < ln; i++) {
                    session = sessions[i];
                    map = session.map;
                    list = session.list;

                    for (j = 0,subLn = propertyNames.length; j < subLn; j++) {
                        name = propertyNames[j];

                        if (map[name]) {
                            delete map[name];
                            Ext.Array.remove(list, name);
                            session.length--;
                            if (--nameMap[name] == 0) {
                                delete nameMap[name];
                                Ext.Array.remove(nameList, name);
                            }
                        }
                    }

                    if (session.length == 0) {
                        sessions.splice(i, 1);
                        i--;
                        ln--;

                        hasCompletedSession = true;
                        this.onAnimationEnd(element, session.data, session.animation, interrupt);
                    }
                }
            }

            if (!replace && !interrupt && sessions.length == 0 && hasCompletedSession) {
                this.onAllAnimationsEnd(element);
            }
        },

        getRunningData: function(id) {
            var runningAnimationsData = this.runningAnimationsData;

            if (!runningAnimationsData.hasOwnProperty(id)) {
                runningAnimationsData[id] = {
                    nameMap: {},
                    nameList: [],
                    sessions: []
                };
            }

            return runningAnimationsData[id];
        },

        getTestElement: function() {
            var testElement = this.testElement,
                iframe, iframeDocument, iframeStyle;

            if (!testElement) {
                iframe = document.createElement('iframe');
                iframeStyle = iframe.style;
                iframeStyle.setProperty('visibility', 'hidden', 'important');
                iframeStyle.setProperty('width', '0px', 'important');
                iframeStyle.setProperty('height', '0px', 'important');
                iframeStyle.setProperty('position', 'absolute', 'important');
                iframeStyle.setProperty('border', '0px', 'important');
                iframeStyle.setProperty('zIndex', '-1000', 'important');

                document.body.appendChild(iframe);
                iframeDocument = iframe.contentDocument;

                iframeDocument.open();
                iframeDocument.writeln('</body>');
                iframeDocument.close();

                this.testElement = testElement = iframeDocument.createElement('div');
                testElement.style.setProperty('position', 'absolute', '!important');
                iframeDocument.body.appendChild(testElement);
                this.testElementComputedStyle = window.getComputedStyle(testElement);
            }

            return testElement;
        },

        getCssStyleValue: function(name, value) {
            var testElement = this.getTestElement(),
                computedStyle = this.testElementComputedStyle,
                style = testElement.style;

            style.setProperty(name, value);
            value = computedStyle.getPropertyValue(name);
            style.removeProperty(name);

            return value;
        },

        run: function(animations) {
            var me = this,
                isLengthPropertyMap = this.lengthProperties,
                fromData = {},
                toData = {},
                data = {},
                element, elementId, from, to, before,
                fromPropertyNames, toPropertyNames,
                doApplyTo, message,
                runningData,
                i, j, ln, animation, propertiesLength, sessionNameMap,
                computedStyle, formattedName, name, toFormattedValue,
                computedValue, fromFormattedValue, isLengthProperty,
                runningNameMap, runningNameList, runningSessions;

            if (!this.listenersAttached) {
                this.attachListeners();
            }

            animations = Ext.Array.from(animations);

            for (i = 0,ln = animations.length; i < ln; i++) {
                animation = animations[i];
                animation = Ext.factory(animation, Ext.fx.Animation);
                element = animation.getElement();

                computedStyle = window.getComputedStyle(element.dom);

                elementId = element.getId();

                data = Ext.merge({}, animation.getData());

                if (animation.onBeforeStart) {
                    animation.onBeforeStart.call(animation.scope || this, element);
                    animation.fireEvent('animationstart', animation);
                }

                data[elementId] = data;

                before = data.before;
                from = data.from;
                to = data.to;

                data.fromPropertyNames = fromPropertyNames = [];
                data.toPropertyNames = toPropertyNames = [];

                for (name in to) {
                    if (to.hasOwnProperty(name)) {
                        to[name] = toFormattedValue = this.formatValue(to[name], name);
                        formattedName = this.formatName(name);
                        isLengthProperty = isLengthPropertyMap.hasOwnProperty(name);

                        if (!isLengthProperty) {
                            toFormattedValue = this.getCssStyleValue(formattedName, toFormattedValue);
                        }

                        if (from.hasOwnProperty(name)) {
                            from[name] = fromFormattedValue = this.formatValue(from[name], name);

                            if (!isLengthProperty) {
                                fromFormattedValue = this.getCssStyleValue(formattedName, fromFormattedValue);
                            }

                            if (toFormattedValue !== fromFormattedValue) {
                                fromPropertyNames.push(formattedName);
                                toPropertyNames.push(formattedName);
                            }
                        }
                        else {
                            computedValue = computedStyle.getPropertyValue(formattedName);

                            if (toFormattedValue !== computedValue) {
                                toPropertyNames.push(formattedName);
                            }
                        }
                    }
                }

                propertiesLength = toPropertyNames.length;

                if (propertiesLength === 0) {
                    this.onAnimationEnd(element, data, animation);
                    continue;
                }

                runningData = this.getRunningData(elementId);
                runningSessions = runningData.sessions;

                if (runningSessions.length > 0) {
                    this.refreshRunningAnimationsData(
                        element, Ext.Array.merge(fromPropertyNames, toPropertyNames), true, data.replacePrevious
                    );
                }

                runningNameMap = runningData.nameMap;
                runningNameList = runningData.nameList;

                sessionNameMap = {};
                for (j = 0; j < propertiesLength; j++) {
                    name = toPropertyNames[j];
                    sessionNameMap[name] = true;

                    if (!runningNameMap.hasOwnProperty(name)) {
                        runningNameMap[name] = 1;
                        runningNameList.push(name);
                    }
                    else {
                        runningNameMap[name]++;
                    }
                }

                runningSessions.push({
                    element: element,
                    map: sessionNameMap,
                    list: toPropertyNames.slice(),
                    length: propertiesLength,
                    data: data,
                    animation: animation
                });

                fromData[elementId] = from = Ext.apply(Ext.Object.chain(before), from);

                if (runningNameList.length > 0) {
                    fromPropertyNames = Ext.Array.difference(runningNameList, fromPropertyNames);
                    toPropertyNames = Ext.Array.merge(fromPropertyNames, toPropertyNames);
                    from['transition-property'] = fromPropertyNames;
                }

                toData[elementId] = to = Ext.Object.chain(to);

                to['transition-property'] = toPropertyNames;
                to['transition-duration'] = data.duration;
                to['transition-timing-function'] = data.easing;
                to['transition-delay'] = data.delay;
            }

            message = this.$className;

            this.applyStyles(fromData);

            doApplyTo = function(e) {
                if (e.data === message && e.source === window) {
                    window.removeEventListener('message', doApplyTo, false);
                    me.applyStyles(toData);
                }
            };

            window.addEventListener('message', doApplyTo, false);
            window.postMessage(message, '*');
        }
    });