
/**
 * Slideshow JS Bundle
 */
YUI.add('fl-event-move', function(Y) {

/**
 * Adds gesturemovevertical, gesturemoveverticalend, gesturemovehorizontal
 * and gesturemovehorizontalend events.
 *
 * @module fl-event-move
 */
var _eventBase = {

	_isEndEvent: false,

	on: function(node, subscriber, ce)
	{
    	if(this.type.indexOf('end') > -1) {
    		this._isEndEvent = true;
    	}

		subscriber._direction = this.type.replace('gesturemove', '').replace('end', '');

		if(window.navigator.msPointerEnabled) {
    		subscriber._startHandle = node.on('MSPointerDown', this._onStart, this, node, subscriber, ce);
        	subscriber._moveHandle = node.on('MSPointerMove', this._onMove, this, node, subscriber, ce);
        	subscriber._endHandle = node.on('MSPointerUp', this._onEnd, this, node, subscriber, ce);
		}
		else {
    	    subscriber._startHandle = node.on('gesturemovestart', this._onStart, null, this, node, subscriber, ce);
        	subscriber._moveHandle = node.on('gesturemove', this._onMove, null, this, node, subscriber, ce);
        	subscriber._endHandle = node.on('gesturemoveend', this._onEnd, { standAlone: true }, this, node, subscriber, ce);
		}
    },

    detach: function(node, subscriber, ce)
    {
        subscriber._startHandle.detach();
        subscriber._startHandle = null;
		subscriber._moveHandle.detach();
        subscriber._moveHandle = null;
		subscriber._endHandle.detach();
		subscriber._endHandle = null;
    },

    _onStart: function(e, node, subscriber, ce)
    {
    	subscriber._doMove = null;
		subscriber._startX = e.pageX;
		subscriber._startY = e.pageY;
	},

	_onMove: function(e, node, subscriber, ce)
	{
		if(this._checkDirection(e, subscriber)) {
			subscriber._doMove = true;
		}
		else {
			subscriber._doMove = false;
		}
		if(subscriber._doMove && !this._isEndEvent) {
			ce.fire(e);
		}
	},

    _onEnd: function(e, node, subscriber, ce)
    {
    	if(subscriber._doMove && this._isEndEvent) {
    		e.startPageX = subscriber._startX;
    		e.startPageY = subscriber._startY;
    		ce.fire(e);
    	}

    	subscriber._doMove = null;
	},

	_checkDirection: function(e, subscriber)
	{
		var xDelta = Math.abs(subscriber._startX - e.pageX),
			yDelta = Math.abs(subscriber._startY - e.pageY);

		if(yDelta > xDelta && subscriber._startY > e.pageY && subscriber._direction == 'vertical') {
			return true;
		}
		else if(yDelta > xDelta && subscriber._startY < e.pageY && subscriber._direction == 'vertical') {
			return true;
		}
		else if(yDelta < xDelta && subscriber._startX > e.pageX && subscriber._direction == 'horizontal') {
			return true;
		}
		else if(yDelta < xDelta && subscriber._startX < e.pageX && subscriber._direction == 'horizontal') {
			return true;
		}

		return false;
	}
};

/**
 * @event gesturemovevertical
 * @param type {String} "gesturemovevertical"
 * @param fn {Function} The method the event invokes.
 * @param ctx {Object} Context for the method the event invokes.
 */
Y.Event.define('gesturemovevertical', _eventBase);

/**
 * @event gesturemoveverticalend
 * @param type {String} "gesturemoveverticalend"
 * @param fn {Function} The method the event invokes.
 * @param ctx {Object} Context for the method the event invokes.
 */
Y.Event.define('gesturemoveverticalend', _eventBase);

/**
 * @event gesturemovehorizontal
 * @param type {String} "gesturemovehorizontal"
 * @param fn {Function} The method the event invokes.
 * @param ctx {Object} Context for the method the event invokes.
 */
Y.Event.define('gesturemovehorizontal', _eventBase);

/**
 * @event gesturemovehorizontalend
 * @param type {String} "gesturemovehorizontalend"
 * @param fn {Function} The method the event invokes.
 * @param ctx {Object} Context for the method the event invokes.
 */
Y.Event.define('gesturemovehorizontalend', _eventBase);


}, '2.0.0' ,{requires:['event-move']});


YUI.add('fl-slideshow', function(Y) {

/**
 * @module fl-slideshow
 */

/**
 * Caption widget used in slideshows.
 *
 * @namespace FL
 * @class SlideshowCaption
 * @constructor
 * @param config {Object} Configuration object
 * @extends Widget
 */
Y.namespace('FL').SlideshowCaption = Y.Base.create('fl-slideshow-caption', Y.Widget, [Y.WidgetChild], {

	/**
	 * Flag for whether the text has been
	 * toggled or not.
	 *
	 * @property _textToggled
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_textToggled: false,

	/**
	 * An anchor node used for the toggle link.
	 *
	 * @property _textToggleLink
	 * @type Object
	 * @default null
	 * @protected
	 */
	_textToggleLink: null,

	/**
	 * @method renderUI
	 * @protected
	 */
	renderUI: function()
	{
        var root = this.get('root'),
		    bb   = this.get('boundingBox'),
				fallback = this.get('root').get('fallback');

		if(fallback) {
			this._textToggleLink = Y.Node.create('<a role="button" tabindex="0"></a>');
		}
		else {
			this._textToggleLink = Y.Node.create('<button type="button"></button>');
		}
		this._textToggleLink.addClass('fl-slideshow-caption-toggle' + (!fallback ? ' fl-content-ui-button' : ''));
		this._textToggleLink.set('innerHTML', root.get('captionMoreLinkText'));

		bb.appendChild(this._textToggleLink);
	},

	/**
	 * @method bindUI
	 * @protected
	 */
	bindUI: function()
	{
		this.get('root').on('imageLoadComplete', Y.bind(this._setText, this));
		this._textToggleLink.on('click', Y.bind(this._toggleText, this));
	},

	/**
	 * Sets the caption text and displays the
	 * toggle link if necessary.
	 *
	 * @method _setText
	 * @protected
	 */

	_setText: function()
	{
		var root 		= this.get('root'),
			text 		= root.imageInfo.caption,
			textLength	= root.get('captionTextLength'),
			cb			= this.get('contentBox');

		if(!root.imageInfo.caption || root.imageInfo.caption === '') {
			cb.set('innerHTML', '');
			this._textToggleLink.setStyle('display', 'none');
			return;
		}
		else if(textLength > -1) {
			if(!this._textToggled && textLength < text.length) {
				text = this._shortenText(text);
				this._textToggleLink.setStyle('display', 'inline-block');
			}
			else if(this._textToggled && textLength < text.length) {
				text = this._stripTags(text);
				this._textToggleLink.setStyle('display', 'inline-block');
			}
			else {
				text = this._stripTags(text);
				this._textToggleLink.setStyle('display', 'none');
			}
		}
		else {
			text = this._stripTags(text);
		}

		cb.set('innerHTML', text);
	},

	/**
	 * Shows or hides the full text when the
	 * toggle link is clicked.
	 *
	 * @method _toggleText
	 * @protected
	 */
	_toggleText: function()
	{
		var root 		= this.get('root'),
			text 		= root.imageInfo.caption,
			cb			= this.get('contentBox');

		if(this._textToggled) {
			text = this._shortenText(text);
			this._textToggleLink.set('innerHTML', root.get('captionMoreLinkText'));
			this._textToggled = false;
		}
		else {
			text = this._stripTags(text);
			this._textToggleLink.set('innerHTML', root.get('captionLessLinkText'));
			this._textToggled = true;
		}

		cb.set('innerHTML', text);
	},

	/**
	 * Strips out HTML tags from the caption text.
	 *
	 * @method _stripTags
	 * @param text {String} The text to strip HTML tags from.
	 * @param ignoreSettings {Boolean} If true, will strip tags even if
	 * the stripTags attribute is set to false.
	 * @protected
	 */
	_stripTags: function(text, ignoreSettings)
	{
        var root = this.get('root'), textDiv;

		if(ignoreSettings || root.get('captionStripTags')) {
			textDiv = document.createElement('div');
			textDiv.innerHTML = text;
			text = textDiv.textContent || textDiv.innerText;
		}

		return text;
	},

	/**
	 * Shortens the caption text to the length of
	 * the textLength attribute.
	 *
	 * @method _shortenText
	 * @protected
	 */
	_shortenText: function(text)
	{
        var root = this.get('root');

		text = this._stripTags(text, true).substring(0, root.get('captionTextLength'));

		return Y.Lang.trim(text.substring(0, text.lastIndexOf(' '))) + ' ...';
	}

}, {

	/**
	 * Custom CSS class name for the widget.
	 *
	 * @property CSS_PREFIX
	 * @type String
	 * @protected
	 * @static
	 */
	CSS_PREFIX: 'fl-slideshow-caption',

	/**
	 * Static property used to define the default attribute configuration of
	 * the Widget.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

	}
});

/**
 * A widget for loading and transitioning between SlideshowImage
 * instances. Each SlideshowImage instance is a child widget of
 * SlideshowFrame. SlideshowFrame is a child widget of the main
 * slideshow widget.
 *
 * @namespace FL
 * @class SlideshowFrame
 * @constructor
 * @param config {Object} Configuration object
 * @extends Widget
 */
Y.namespace('FL').SlideshowFrame = Y.Base.create('fl-slideshow-frame', Y.Widget, [Y.WidgetParent, Y.WidgetChild], {

	/**
	 * The imageInfo object used to load the active image.
	 *
	 * @property info
	 * @type Object
	 * @default null
	 * @protected
	 */
	_imageInfo: null,

	/**
	 * The active FL.SlideshowImage instance in the frame.
	 *
	 * @property _activeImage
	 * @type FL.SlideshowImage
	 * @default null
	 * @protected
	 */
	_activeImage: null,

	/**
	 * A FL.SlideshowImage instance used to load the
	 * next image and transition it into the frame.
	 *
	 * @property _nextImage
	 * @type FL.SlideshowImage
	 * @default null
	 * @protected
	 */
	_nextImage: null,

	/**
	 * Used to store imageInfo if a load request is
	 * made while the frame is transitioning. If not null
	 * when the transition completes, a new image will
	 * be loaded using the imageInfo.
	 *
	 * @property _loadQueue
	 * @type Object
	 * @default false
	 * @protected
	 */
	_loadQueue: null,

	/**
	 * An instance of FL.SlideshowTransition used for
	 * the current transition in progress.
	 *
	 * @property _transition
	 * @type FL.SlideshowTransition
	 * @default null
	 * @protected
	 */
	_transition: null,

	/**
	 * A flag for whether the frame is currently transitioning or not.
	 *
	 * @property _transitioning
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_transitioning: false,

	/**
	 * Flag for whether to resize when the current transition
	 * completes. Set to true when a resize request is made
	 * during a transition.
	 *
	 * @property _resizeAfterTransition
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_resizeAfterTransition: false,

	/**
	 * Provides functionality for gesture based transitions
 	 * between the active and next images.
	 *
	 * @property _gestures
	 * @type FL.SlideshowGestures
	 * @default null
	 * @protected
	 */
	_gestures: null,

	/**
	 * Creates new instances of FL.SlideshowImage used in the frame.
	 *
	 * @method initializer
	 * @protected
	 */
	initializer: function()
	{
		var imageConfig = this.get('imageConfig');

		this._activeImage = new Y.FL.SlideshowImage(imageConfig);
		this._nextImage = new Y.FL.SlideshowImage(imageConfig);
	},

	/**
	 * Renders the FL.SlideshowImage instances used in the frame.
	 *
	 * @method renderUI
	 * @protected
	 */
	renderUI: function()
	{
		this.add(this._activeImage);
		this.add(this._nextImage);
	},

	/**
	 * @method bindUI
	 * @protected
	 */
	bindUI: function()
	{
		var activeBB 	= this._activeImage.get('boundingBox'),
			nextBB 		= this._nextImage.get('boundingBox'),
			transition	= this.get('transition');

		if(('ontouchstart' in window || window.navigator.msPointerEnabled) && this.get('touchSupport')) {

			this._gestures = new Y.FL.SlideshowGestures({
				direction: transition == 'slideVertical' ? 'vertical' : 'horizontal',
				activeItem: activeBB,
				nextItem: nextBB
			});

			this._gestures.on('moveStart', this._gesturesMoveStart, this);
			this._gestures.on('endComplete', this._gesturesEndComplete, this);
		}
	},

	/**
	 * Functional styles for the UI.
	 *
	 * @method syncUI
	 * @protected
	 */
	syncUI: function()
	{
		var activeBB 	= this._activeImage.get('boundingBox'),
			nextBB	 	= this._nextImage.get('boundingBox'),
			cb			= this.get('contentBox');

		activeBB.setStyle('position', 'absolute');
		activeBB.setStyle('top', '0px');
		activeBB.setStyle('left', '-9999px');

		nextBB.setStyle('position', 'absolute');
		nextBB.setStyle('top', '0px');
		nextBB.setStyle('left', '-9999px');

		cb.setStyle('position', 'relative');
		cb.setStyle('overflow', 'hidden');
	},

	/**
	 * Checks whether the imageInfo should be loaded or queued.
	 * Initializes a new transition if loading is ok.
	 *
	 * @method load
	 * @param imageInfo {Object} The image info to load.
	 */
	load: function(imageInfo)
	{
		var activeInfo = this._activeImage._imageInfo;

		if(this._transitioning) {
			this._loadQueue = imageInfo;
			return;
		}
		else if(activeInfo && activeInfo.largeURL == imageInfo.largeURL) {
			return;
		}

		this._imageInfo = imageInfo;
		this._transitionInit(imageInfo);
	},

	/**
	 * Preloads the next image using the provided imageInfo.
	 *
	 * @method preload
	 * @param imageInfo {Object} The imageInfo to preload.
	 * @param width {Number} The width to preload.
	 * @param height {Number} The height to preload.
	 */
	preload: function(imageInfo, width, height)
	{
		this._imageInfo = imageInfo;
		this._nextImage.preload(imageInfo, width, height);
	},

	/**
	 * Unloads the active and next image instances.
	 *
	 * @method unload
	 */
	unload: function()
	{
		this._imageInfo = null;
		this._loadQueue = null;
		this._transitioning = false;
		this._transition = null;

		this._activeImage.detachAll();
		this._activeImage.unload();
		this._activeImage.get('boundingBox').setStyle('left', '-9999px');

		this._nextImage.detachAll();
		this._nextImage.unload();
		this._nextImage.get('boundingBox').setStyle('left', '-9999px');
	},

	/**
	 * Resizes the bounding box and active image.
	 *
	 * @method resize
	 * @param width {Number} The width value.
	 * @param height {Number} The height value.
	 */
	resize: function(width, height)
	{
		if(!width || !height) {
			return;
		}

		var bb 		= this.get('boundingBox'),
			padding = [
				parseInt(bb.getComputedStyle('paddingTop'), 10),
				parseInt(bb.getComputedStyle('paddingRight'), 10),
				parseInt(bb.getComputedStyle('paddingBottom'), 10),
				parseInt(bb.getComputedStyle('paddingLeft'), 10)
			];

		width = width - padding[1] - padding[3];
		height = height - padding[0] - padding[2];

		this.set('width', width);
		this.set('height', height);

		if(this._transitioning) {
			this._resizeAfterTransition = true;
		}
		else {
			this._activeImage.resize(width, height);
			this._nextImage.resize(width, height);
		}
	},

	/**
	 * Gets the current transition to use.
	 *
	 * @method _getTransition
	 * @protected
	 */
	_getTransition: function()
	{
		var root            = this.get('root'),
            lastIndex 		= root.albumInfo.images.length - 1,
			direction		= 'next',
			transition 		= root.get('transition');

		if(root.lastImageIndex === null) {
			direction = '';
		}
		else if(root.imageIndex == lastIndex && root.lastImageIndex === 0) {
			direction = 'prev';
		}
		else if(root.imageIndex === 0 && root.lastImageIndex == lastIndex) {
			direction = 'next';
		}
		else if(root.lastImageIndex > root.imageIndex) {
			direction = 'prev';
		}
		else if(root.lastImageIndex < root.imageIndex) {
			direction = 'next';
		}

		if(direction == 'next') {
			transition = transition.replace('slideHorizontal', 'slideLeft');
			transition = transition.replace('slideVertical', 'slideUp');
		}
		else if(direction == 'prev') {
			transition = transition.replace('slideHorizontal', 'slideRight');
			transition = transition.replace('slideVertical', 'slideDown');
		}

		return transition;
	},

	/**
	 * Fires the transitionInit event and loads the next image.
	 * The transition starts when the image's loadComplete
	 * event is fired.
	 *
	 * @method _transitionInit
	 * @param imageInfo {Object} The imageInfo to load before transitioning.
	 * @protected
	 */
	_transitionInit: function(imageInfo)
	{
		this._transitioning = true;

		// Disable gestures if set.
		if(this._gestures) {
			this._gestures.disable();
		}

		/**
		 * Fires when the next image is loading before a new transition.
		 *
		 * @event transitionInit
		 */
		this.fire('transitionInit');

		if(imageInfo) {
			this._nextImage.once('loadComplete', this._transitionStart, this);
			this._nextImage.load(imageInfo);
		}
		else {
			this._transitionStart();
		}
	},

	/**
	 * Fires the transitionStart event and starts the transition
	 * using a new instance of FL.SlideshowTransition.
	 *
	 * @method _transitionStart
	 * @protected
	 */
	_transitionStart: function()
	{
        var root = this.get('root');

		/**
		 * Fires when the next image has finished loading
		 * and a new transition starts.
		 *
		 * @event transitionStart
		 */
		this.fire('transitionStart');

		this._transition = new Y.FL.SlideshowTransition({
			itemIn: this._nextImage._imageInfo ? this._nextImage.get('boundingBox') : null,
			itemOut: this._activeImage._imageInfo ? this._activeImage.get('boundingBox') : null,
			type: this._getTransition(),
			duration: root.get('transitionDuration'),
			easing: root.get('transitionEasing'),
			kenBurnsDuration: root.get('speed')/1000,
			kenBurnsZoom: root.get('kenBurnsZoom')
		});

		if(this._nextImage._imageInfo) {
			this._nextImage.get('boundingBox').setStyle('left', '0px');
		}

		this._transition.once('complete', this._transitionComplete, this);
		this._transition.run();
	},

	/**
	 * Switches the next and active image variables, unloads the
	 * last image, fires the transitionComplete event and loads
	 * or resizes if appropriate.
	 *
	 * @method _transitionComplete
	 * @protected
	 */
	_transitionComplete: function()
	{
        var root = this.get('root');

        // Swap image container references.
		this._swapImageRefs();

		/**
		 * Fired when the current transition completes.
		 *
		 * @event transitionComplete
		 */
		this.fire('transitionComplete');
		this._transition = null;
		this._transitioning = false;

		// Enable gestures if set.
		if(this._gestures) {
            if(root && root.albumInfo.images.length <= 1) {
                this._gestures.disable();
            }
            else {
                this._gestures.enable();
            }
		}

		// Load from the queue?
		if(this._loadQueue) {
			this.load(this._loadQueue);
			this._loadQueue = null;
		}
		// Resize the active image?
		else if(this._resizeAfterTransition) {
			this._resizeAfterTransition = false;
			this._activeImage.resize(this.get('width'), this.get('height'));
			this._nextImage.resize(this.get('width'), this.get('height'));
		}
	},

	/**
	 * @method _gesturesMoveStart
	 * @param e {Object} The event object.
	 * @protected
	 */
	_gesturesMoveStart: function(e)
	{
		var index 	= 0,
			root 	= this.get('root');

		index = e.direction == 'next' ? root.imageIndex + 1 : root.imageIndex - 1;
        index = index < 0 ? root.albumInfo.images.length - 1 : index;
		index = index >= root.albumInfo.images.length ? 0 : index;

		root.pause();
		root._hideLoadingImage();
		root._showLoadingImageWithDelay();

		Y.FL.SlideshowImageLoader.removeGroup(this._nextImage.get('loadGroup'));

		this._nextImage.once('loadComplete', root._hideLoadingImage, root);
		this._nextImage.load(root.albumInfo.images[index]);
	},

	/**
	 * @method _gesturesEndComplete
	 * @protected
	 */
	_gesturesEndComplete: function()
	{
		var root	= this.get('root'),
			index	= 0;

        if(this._nextImage._imageInfo){
        	index = this._nextImage._imageInfo.index;
        	this._swapImageRefs();
			this._imageInfo = root.albumInfo.images[index];
			root.loadImage(index);
        }
	},

	/**
	 * @method _swapImageRefs
	 * @protected
	 */
	_swapImageRefs: function()
	{
		var active = this._activeImage;
		this._activeImage = this._nextImage;
		this._nextImage = active;

		if(this._nextImage._imageInfo) {
			this._nextImage.unload();
			this._nextImage.get('boundingBox').setStyle('left', '-9999px');
		}
		if(this._gestures) {
			this._gestures.set('activeItem', this._activeImage.get('boundingBox'));
			this._gestures.set('nextItem', this._nextImage.get('boundingBox'));
		}
	}

}, {

	/**
	 * Custom CSS class name for the widget.

	 * @property CSS_PREFIX
	 * @type String
	 * @protected
	 * @static
	 */
	CSS_PREFIX: 'fl-slideshow-frame',

	/**
	 * Static property used to define the default attribute configuration of
	 * the Widget.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

		/**
		 * The configuration object used to create new instances of
		 * FL.SlideshowImage. See the API docs for {@link FL.SlideshowImage}
		 * for a complete list of configuration attributes.
		 *
		 * @attribute imageConfig
		 * @type Object
		 * @default null
		 */
		imageConfig: {
			value: null
		},

		/**
		 * Whether to use touch gestures, when available,
		 * to transition between images or not.
		 *
		 * @attribute touchSupport
		 * @type Boolean
		 * @default false
		 */
		touchSupport: {
			value: false
		}
	}
});

/**
 * A plugin for fullscreen slideshow functionality.
 *
 * @namespace FL
 * @class SlideshowFullscreen
 * @constructor
 * @param config {Object} Configuration object
 * @extends Plugin.Base
 */
Y.namespace('FL').SlideshowFullscreen = Y.Base.create('fl-slideshow-fullscreen', Y.Plugin.Base, [], {

	/**
	 * Flag for whether the slideshow is in
	 * fullscreen mode.
	 *
	 * @property active
	 * @type Boolean
	 * @default false
	 */
	active: false,

	/**
	 * A div containing the close message.
	 *
	 * @property _closeMessage
	 * @type Node
	 * @default null
	 * @protected
	 */
	_closeMessage: null,

	/**
	 * A timer for hiding the close message.
	 *
	 * @property _closeMessageTimer
	 * @type Object
	 * @default null
	 * @protected
	 */
	_closeMessageTimer: null,

	/**
	 * The initial styles of the host's bounding box
	 * before entering fullscreen mode.
	 *
	 * @property _initialStyles
	 * @type Object
	 * @protected
	 */
	_initialStyles: {
		position: 'static',
		top: '0px',
		left: '0px'
	},

	/**
	 * @method initializer
	 * @protected
	 */
	initializer: function()
	{
		var host 	= this.get('host'),
			bb 		= host.get('boundingBox'),
			self 	= this;

		bb.addClass('fl-fullscreen-enabled');

		if(Y.FL.SlideshowFullscreen.OS_SUPPORT) {
			document.addEventListener('fullscreenchange', function(){ self._osChange(); }, false);
			document.addEventListener('mozfullscreenchange', function(){ self._osChange(); }, false);
			document.addEventListener('webkitfullscreenchange', function(){ self._osChange(); }, false);
		}
		else {
			this._renderCloseMessage();
		}
    },

	/**
	 * Exits fullscreen if it is currently active
	 * otherwise it enters fullscreen.
	 *
	 * @method toggle
	 */
	toggle: function()
	{
		if(this.active) {
			this.exit();
		}
		else {
			this.enter();
		}
	},

	/**
	 * Enters OS fullscreen mode if supported, otherwise
	 * the slideshow takes over the browser window.
	 *
	 * @method enter
	 */
	enter: function()
	{
		if(Y.FL.SlideshowFullscreen.OS_SUPPORT) {
			this._osEnter();
		}
		else {
			this._browserEnter();
		}
	},

	/**
	 * Exits fullscreen mode.
	 *
	 * @method exit
	 */
	exit: function()
	{
		if(Y.FL.SlideshowFullscreen.OS_SUPPORT) {
			this._osExit();
		}
		else {
			this._browserExit();
		}
	},

	/**
	 * Enters OS fullscreen mode.
	 *
	 * @method _osEnter
	 * @protected
	 */
	_osEnter: function()
	{
		var bbNode = this.get('host').get('boundingBox')._node;

		if(bbNode.webkitRequestFullScreen) {
			bbNode.webkitRequestFullScreen();
		}
		else if(bbNode.mozRequestFullScreen) {
			bbNode.mozRequestFullScreen();
		}
		else if(bbNode.requestFullScreen) {
			bbNode.requestFullScreen();
		}
	},

	/**
	 * Exits OS fullscreen mode.
	 *
	 * @method _osExit
	 * @protected
	 */
	_osExit: function()
	{
		if(document.exitFullscreen) {
            document.exitFullscreen();
        }
        else if(document.mozCancelFullScreen) {
            document.mozCancelFullScreen();
        }
        else if(document.webkitCancelFullScreen) {
            document.webkitCancelFullScreen();
        }
	},

	/**
	 * Called when the OS fullscreenchange event fires and enters
	 * or exits standard fullscreen mode which positions and
	 * resizes the slideshow.
	 *
	 * @method _osChange
	 * @protected
	 */
	_osChange: function()
	{
		var host = this.get('host');

		// Transitions break on Safari while entering and
		// exiting fullscreen. This fixes them!
		if(host.frame && host.frame._transitioning) {
			host.frame._transitionComplete();
		}

		if(this.active) {
			this._exit();
		}
		else {
			this._enter();
		}
	},

	/**
	 * Enter browser fullscreen mode.
	 *
	 * @method _browserEnter
	 * @protected
	 */
	_browserEnter: function()
	{
		var bb = this.get('host').get('boundingBox');

		this._initialStyles = {
    		position: bb.getStyle('position'),
    		top: bb.getStyle('top'),
    		left: bb.getStyle('left'),
    		zIndex: bb.getStyle('zIndex')
    	};

		bb.setStyles({
			position: 'fixed',
			top: '0px',
			left: '0px',
			zIndex: 10000
		});

		Y.Node.one('body').on('fl-fullscreen|keydown', Y.bind(this._onKey, this));

		this._showCloseMessage();
		this._enter();
	},

	/**
	 * Exit browser fullscreen mode.
	 *
	 * @method _browserExit
	 * @protected
	 */
	_browserExit: function()
	{
		var bb = this.get('host').get('boundingBox');

		bb.setStyles({
			position: this._initialStyles.position,
			top: this._initialStyles.top,
			left: this._initialStyles.left,
			zIndex: this._initialStyles.zIndex
		});

		Y.Node.one('body').detach('fl-fullscreen|keydown');

		this._hideCloseMessage();
		this._exit();
	},

	/**
	 * Enters fullscreen mode.
	 *
	 * @method _enter
	 * @protected
	 */
	_enter: function()
	{
		var host 	= this.get('host'),
			bb 		= host.get('boundingBox');

		bb.addClass('fl-fullscreen-active');

		this.active = true;

		host.resize();
	},

	/**
	 * Exits fullscreen mode.
	 *
	 * @method _exit
	 * @protected
	 */
	_exit: function()
	{
		var host 	= this.get('host'),
			bb 		= host.get('boundingBox');

		bb.removeClass('fl-fullscreen-active');

		this.active = false;

		host.resize();
	},

	/**
	 * Keyboard input for the esc button.
	 *
	 * @method _onKey
	 * @protected
	 */
	_onKey: function(e)
	{
		if(e.keyCode == 27) {
			this.exit();
			return false;
		}
	},

	/**
	 * Creates the close message if one is
	 * not already available in the document.
	 *
	 * @method _initCloseMessage
	 * @protected
	 */
	_renderCloseMessage: function()
	{
		this._closeMessage = Y.Node.create('<div class="fl-fullscreen-close-message"></div>');
		this._closeMessage.set('innerHTML', '<span>Press the "esc" button to exit fullscreen mode.</span>');
		this._closeMessage.setStyle('display', 'none');
		this.get('host').get('boundingBox').insert(this._closeMessage);
	},

	/**
	 * Shows the close message.
	 *
	 * @method _showCloseMessage
	 * @protected
	 */
	_showCloseMessage: function()
	{
		if(this._closeMessageTimer) {
			this._closeMessageTimer.cancel();
			this._closeMessageTimer = null;
		}

		this._closeMessage.show(true);
		this._closeMessageTimer = Y.later(4000, this, this._hideCloseMessage);
	},

	/**
	 * Hides the close message.
	 *
	 * @method _hideCloseMessage
	 * @protected
	 */
	_hideCloseMessage: function()
	{
		if(this._closeMessageTimer) {
			this._closeMessageTimer.cancel();
			this._closeMessageTimer = null;
		}

		this._closeMessage.hide(true);
	}

},	{

	/**
	 * Namespace for the plugin.
	 *
	 * @property NS
	 * @type String
	 * @protected
	 * @static
	 */
	NS: 'fullscreen',

	OS_SUPPORT: (function(){

		var doc = document.documentElement;

		return doc.webkitRequestFullScreen || doc.mozRequestFullScreen || doc.requestFullScreen;
	})()
});

/**
 * Provides functionality for gesture based transitions
 * between two slideshow components.
 *
 * @namespace FL
 * @class SlideshowGestures
 * @constructor
 * @param config {Object} Configuration object
 * @extends Base
 */
Y.namespace('FL').SlideshowGestures = Y.Base.create('fl-slideshow-gestures', Y.Base, [], {

	/**
	 * The x coordinate for where a gesture event starts.
	 *
	 * @property _startX
	 * @type Number
	 * @default null
	 * @protected
	 */
	_startX: null,

	/**
	 * The y coordinate for where a gesture event starts.
	 *
	 * @property _startY
	 * @type Number
	 * @default null
	 * @protected
	 */
	_startY: null,

	/**
	 * A flag for whether a gesture is moving or not.
	 *
	 * @property _moving
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_touchMoving: false,

	/**
	 * Whether the gesture is moving or not.
	 *
	 * @property _moving
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_moving: false,

	/**
	 * The direction the current gesture event
	 * is moving in (either next or prev).
	 *
	 * @property _movingDirection
	 * @type String
	 * @default null
	 * @protected
	 */
	_movingDirection: null,

	/**
	 * A flag for whether a gesture gesture is currently
	 * transitioning or not.
	 *
	 * @property _transitioning
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_transitioning: false,

	/**
	 * @method initializer
	 * @protected
	 */
	initializer: function()
	{
		this.enable();
	},

	/**
	 * @method enable
	 */
	enable: function()
	{
		var id 			= this.get('id'),
			direction 	= this.get('direction'),
			active 		= this.get('activeItem'),
			next 		= this.get('nextItem');

		active.on(id + '|gesturemovestart', Y.bind(this._onStart, this));
		next.on(id + '|gesturemovestart', Y.bind(this._onStart, this));
		next.on(id + '|transitionend', Y.bind(this._onEndComplete, this) );
		next.on(id + '|oTransitionEnd', Y.bind(this._onEndComplete, this) );
		next.on(id + '|webkitTransitionEnd', Y.bind(this._onEndComplete, this) );

		if(direction == 'horizontal') {
			active.on(id + '|gesturemovehorizontal', Y.bind(this._onMoveHorizontal, this));
			active.on(id + '|gesturemovehorizontalend', Y.bind(this._onEndHorizontal, this));
			next.on(id + '|gesturemovehorizontal', Y.bind(this._onMoveHorizontal, this));
			next.on(id + '|gesturemovehorizontalend', Y.bind(this._onEndHorizontal, this));
		}
		else {
			active.on(id + '|gesturemovevertical', Y.bind(this._onMoveVertical, this));
			active.on(id + '|gesturemoveverticalend', Y.bind(this._onEndVertical, this));
			next.on(id + '|gesturemovevertical', Y.bind(this._onMoveVertical, this));
			next.on(id + '|gesturemoveverticalend', Y.bind(this._onEndVertical, this));
		}
	},

	/**
	 * @method disable
	 */
	disable: function()
	{
		var id 		= this.get('id'),
			active 	= this.get('activeItem'),
			next 	= this.get('nextItem');

		active.detach(id + '|*');
		next.detach(id + '|*');
	},

	/**
	 * @method _onStart
	 * @param e {Object} The event object.
	 * @protected
	 */
	_onStart: function(e)
	{
		var direction = this.get('direction');

		if(this._transitioning) {
			this._onEndComplete();
		}

		if(direction == 'horizontal') {
			this._startX = e.pageX;
		}
		else {
			this._startY = e.pageY;
		}

		/**
		 * @event start
		 */
		this.fire('start');
	},

	/**
	 * @method _onMoveHorizontal
	 * @param e {Object} The event object.
	 * @protected
	 */
	_onMoveHorizontal: function(e)
	{
		var x 			= this._startX - e.pageX,
			active 		= this.get('activeItem'),
			next 		= this.get('nextItem'),
			width		= parseInt(active.getComputedStyle('width'), 10),
			translate 	= x < 0 ? Math.abs(x) : -x,
			direction	= x < 0 ? 'prev' : 'next';

		e.preventDefault();

		if(!this._moving || this._movingDirection != direction) {

        	active.setStyle('left', 0);

	        next.setStyles({
	        	'opacity': 1,
	        	'left': x < 0 ? -width : width
	        });

	        this._moving = true;
        	this._movingDirection = direction;

			/**
			 * @event moveStart
			 */
			this.fire('moveStart', { direction: direction });
		}

		active.setStyle('-webkit-transform', 'translate('+ translate +'px, 0px) translateZ(0px)');
		active.setStyle('-ms-transform', 'translate('+ translate +'px, 0px) translateZ(0px)');
        next.setStyle('-webkit-transform', 'translate('+ translate +'px, 0px) translateZ(0px)');
        next.setStyle('-ms-transform', 'translate('+ translate +'px, 0px) translateZ(0px)');

		/**
		 * @event move
		 */
		this.fire('move');
	},

	/**
	 * @method _onMoveVertical
	 * @param e {Object} The event object.
	 * @protected
	 */
	_onMoveVertical: function(e)
	{
		var y 			= this._startY - e.pageY,
			active 		= this.get('activeItem'),
			next 		= this.get('nextItem'),
			height		= parseInt(active.getComputedStyle('height'), 10),
			translate 	= y < 0 ? Math.abs(y) : -y,
			direction	= y < 0 ? 'prev' : 'next';

		e.preventDefault();

		if(!this._moving || this._movingDirection != direction) {

        	active.setStyle('top', 0);

	        next.setStyles({
	        	'opacity': 1,
	        	'left' : 'auto',
	        	'top': y < 0 ? -height : height
	        });

	        this._moving = true;
        	this._movingDirection = direction;

			/**
			 * @event moveStart
			 */
			this.fire('moveStart', { direction: direction });
		}

		active.setStyle('-webkit-transform', 'translate(0px, '+ translate +'px) translateZ(0px)');
		active.setStyle('-ms-transform', 'translate(0px, '+ translate +'px) translateZ(0px)');
        next.setStyle('-webkit-transform', 'translate(0px, '+ translate +'px) translateZ(0px)');
        next.setStyle('-ms-transform', 'translate(0px, '+ translate +'px) translateZ(0px)');

		/**
		 * @event move
		 */
		this.fire('move');
	},

	/**
	 * @method _onEndHorizontal
	 * @param e {Object} The event object.
	 * @protected
	 */
	_onEndHorizontal: function(e)
	{
		if(!this._moving) {
			return;
		}

		var x 			= this._startX - e.pageX,
			active 		= this.get('activeItem'),
			next 		= this.get('nextItem'),
			width		= parseInt(next.getComputedStyle('width'), 10),
			translate 	= x < 0 ? width : -width;

		active.transition({
			'transform': 'translate('+ translate +'px, 0px)'
		});

		next.transition({
			'transform': 'translate('+ translate +'px, 0px)'
		});

		this._transitioning = true;

		/**
		 * @event end
		 */
		this.fire('end');
	},

	/**
	 * @method _onEndVertical
	 * @param e {Object} The event object.
	 * @protected
	 */
	_onEndVertical: function(e)
	{
		if(!this._moving) {
			return;
		}

		var y 			= this._startY - e.pageY,
			active 		= this.get('activeItem'),
			next 		= this.get('nextItem'),
			height		= parseInt(next.getComputedStyle('height'), 10),
			translate 	= y < 0 ? height : -height;

		active.transition({
			'transform': 'translate(0px, '+ translate +'px)'
		});

		next.transition({
			'transform': 'translate(0px, '+ translate +'px)'
		});

		this._transitioning = true;

		/**
		 * @event end
		 */
		this.fire('end');
	},

	/**
	 * @method _onEndComplete
	 * @protected
	 */
	_onEndComplete: function()
	{
		var direction 	= this.get('direction'),
			active 		= this.get('activeItem'),
			next 		= this.get('nextItem');

		active.setStyles({
        	'opacity': 0,
        	'-webkit-transform': '',
        	'-webkit-transition': '',
        	'-ms-transform': '',
        	'-ms-transition': ''
        });

		next.setStyles({
        	'-webkit-transform': '',
        	'-webkit-transition': '',
        	'-ms-transform': '',
        	'-ms-transition': ''
        });

        if(direction == 'horizontal') {
        	active.setStyle('left', '-9999px');
        	next.setStyle('left', '0px');
        }
        else {
        	active.setStyle('top', '-9999px');
        	next.setStyle('top', '0px');
        }

		this.set('activeItem', next);
		this.set('nextItem', active);
		this._moving = false;
		this._movingDirection = null;
		this._transitioning = false;

		/**
		 * @event endComplete
		 */
		this.fire('endComplete');
	}

}, {

	/**
	 * Static property used to define the default attribute configuration of
	 * the Widget.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

		/**
		 * The gesture direction to use. Possible values are
		 * horizontal and vertical.
		 *
		 * @attribute direction
		 * @type String
		 * @default horizontal
		 */
		direction: {
			value: 'horizontal'
		},

		/**
		 * The Node that is currently visible.
		 *
		 * @attribute activeItem
		 * @type Node
		 * @default null
		 */
		activeItem: {
			value: null
		},

		/**
		 * The Node that will be transitioned in.
		 *
		 * @attribute nextItem
		 * @type Node
		 * @default null
		 */
		nextItem: {
			value: null
		}
	}
});

/**
 * A load queue for slideshow images.
 *
 * @namespace FL
 * @class SlideshowImageLoader
 * @static
 */
Y.namespace('FL').SlideshowImageLoader = {

	/**
	 * Whether an image is being loaded or not.
	 *
	 * @property _loading
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_loading: false,

	/**
	 * An node for loading the next image.
	 *
	 * @property _currentImage
	 * @type Node
	 * @default null
	 * @protected
	 */
	_currentImage: null,

	/**
	 * An object containing the group, src and callback
	 * for the current image that is being loaded.
	 *
	 * @property _currentImageData
	 * @type Object
	 * @default null
	 * @protected
	 */
	_currentImageData: null,

	/**
	 * An array of image data objects that contain the group,
	 * src and callback for each image that will be loaded.
	 *
	 * @property _queue
	 * @type Array
	 * @default []
	 * @protected
	 */
	_queue: [],

	/**
	 * Adds an image to the queue.
	 *
	 * @method add
	 * @param group {String} The group this image is associated with.
	 * Used to remove images in bulk.
	 * @param src {String} The image url to load.
	 * @param callback {Function} A function to call when the image
	 * has finished loading.
	 * @param bump {Boolean} If true, the image will be added to
	 * the first position in the queue.
	 */
	add: function(group, src, callback, bump)
	{
		var imgData = {
			group		: group,
			src			: src,
			callback	: callback
		};

		if(bump) {
			this._queue.unshift(imgData);
		}
		else {
			this._queue.push(imgData);
		}

		if(!this._loading) {
			this._load();
		}
	},

	/**
	 * Removes a group of images from the queue.
	 *
	 * @method removeGroup
	 * @param group {String} The group to remove.
	 */
	removeGroup: function(group)
	{
		var i = this._queue.length - 1;

		for( ; i > -1 ; i--) {
			if(this._queue[i].group == group) {
				this._queue.splice(i, 1);
			}
		}

		if(this._currentImageData && this._currentImageData.group == group) {
			this._currentImage.detachAll();
			this._currentImage = null;
			this._currentImageData = null;

			if(this._queue.length > 0) {
				this._load();
			}
			else {
				this._loading = false;
			}
		}
	},

	/**
	 * Loads the next image in the queue.
	 *
	 * @method _load
	 * @protected
	 */
	_load: function()
	{
		this._loading = true;
		this._currentImageData = this._queue.shift();
		this._currentImage = Y.Node.create('<img />');
		this._currentImage.on('error', Y.bind(this._loadComplete, this));
		this._currentImage.on('load', Y.bind(this._loadComplete, this));
		this._currentImage.set('src', this._currentImageData.src);
	},

	/**
	 * Calls the current image's callback function if set
	 * and loads the next image if the queue is not empty.
	 *
	 * @method _loadComplete
	 * @protected
	 */
	_loadComplete: function()
	{
		if(this._currentImageData.callback) {
			this._currentImageData.callback(this._currentImage);
		}

		if(this._queue.length > 0) {
			this._load();
		}
		else {
			this._loading = false;
			this._currentImage = null;
			this._currentImageData = null;
		}
	}
};

/**
 * Loads an image or video using the provided imageInfo object.
 *
 * @namespace FL
 * @class SlideshowImage
 * @constructor
 * @param config {Object} Configuration object
 * @extends Widget
 */
Y.namespace('FL').SlideshowImage = Y.Base.create('fl-slideshow-image', Y.Widget, [Y.WidgetChild], {

	/**
	 * The imageInfo object used to load the image and
	 * its various sizes.
	 *
	 * @property info
	 * @type Object
	 * @default null
	 * @protected
	 */
	_imageInfo: null,

	/**
	 * A reference to the current image node in the bounding box.
	 *
	 * @property _image
	 * @type Node
	 * @default null
	 * @protected
	 */
	_image: null,

	/**
	 * Whether or not new imageInfo is loading.
	 *
	 * @property _loading
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_loading: false,

	/**
	 * The URL that is currently being loaded.
	 *
	 * @property _loadingURL
	 * @type Boolean
	 * @default null
	 * @protected
	 */
	_loadingURL: null,

	/**
	 * An anchor node used for the video play button.
	 *
	 * @property _videoButton
	 * @type Node
	 * @default null
	 * @protected
	 */
	_videoButton: null,

	/**
	 * A div node used to hold the video iframe.
	 *
	 * @property _videoBox
	 * @type Node
	 * @default null
	 * @protected
	 */
	_videoBox: null,

	/**
	 * An iframe node used to render the video.
	 *
	 * @property _video
	 * @type Node
	 * @default null
	 * @protected
	 */
	_video: null,

	/**
	 * The default content template for the image
	 * inherited from Y.Widget. Set to null since
	 * only the bounding box is needed.
	 *
	 * @property CONTENT_TEMPLATE
	 * @type String
	 * @default null
	 * @protected
	 */
	CONTENT_TEMPLATE: null,

	/**
	 * Initial styling for the bounding box.
	 *
	 * @method syncUI
	 * @protected
	 */
	syncUI: function()
	{
		var bb = this.get('boundingBox');

		if(this.get('crop')) {
			bb.setStyle('overflow', 'hidden');
			bb.addClass('fl-slideshow-image-cropped');
		}
	},

	/**
	 * Sets the imageInfo object and
	 * loads the appropriate image size.
	 *
	 * @method load
	 * @param imageInfo {Object} The imageInfo object.
	 */
	load: function(imageInfo)
	{
		this._imageInfo = imageInfo;
		this._loading = true;
		this._load();
	},

	/**
	 * Sets the width and height of the bounding box and
	 * preloads an image using the provided imageInfo object.
	 *
	 * @method preload
	 * @param imageInfo {Object} The imageInfo to preload.
	 * @param width {Number} The width to preload.
	 * @param height {Number} The height to preload.
	 */
	preload: function(imageInfo, width, height)
	{
		var isVideo			= this._isVideo(),
			loadVideos 		= this.get('loadVideos'),
			showVideoButton = this.get('showVideoButton');

		this.unload();
		this.set('width', width);
		this.set('height', height);
		this._imageInfo = imageInfo;

		if(!isVideo || !loadVideos || (isVideo && loadVideos && showVideoButton)) {
			Y.FL.SlideshowImageLoader.add(
				this.get('loadGroup'),
				this._getImageURL(),
				Y.bind(this._imagePreloaded, this),
				this.get('loadPriority')
			);
		}
	},

	/**
	 * Called when preloading completes.
	 *
	 * @method _imagePreloaded
	 * @param img {Object} The image that was preloaded.
	 * @protected
	 */
	_imagePreloaded: function(img)
	{
		this._image = img;
	},

	/**
	 * Unloads the image if there is one loaded
	 * and sets the imageInfo object to null.
	 *
	 * @method unload
	 */
	unload: function()
	{
		if(this._image) {
			this._image.remove();
			this._image.detachAll();
			this._image.set('src', '');
			this._image = null;
		}
		if(this._video) {
			this._video.remove();
			this._video = null;
		}
		if(this._videoButton) {
			this._videoButton.remove();
			this._videoButton = null;
		}
		if(this._videoBox) {
			this._removeVideoBox();
		}

		this._imageInfo = null;
		this._loading = false;
		this._loadingURL = null;
	},

	/**
	 * Resizes the bounding box and loads the
	 * appropriate image size if necessary.
	 *
	 * @method resize
	 * @param width {Number} The width value.
	 * @param height {Number} The height value.
	 */
	resize: function(width, height)
	{
		var borderWidth = parseInt(this.get('boundingBox').getComputedStyle('borderTopWidth'), 10) * 2,
			bb			= this.get('boundingBox');

		this.set('width', width - borderWidth);
		this.set('height', height - borderWidth);
		bb.setStyle('width', width - borderWidth + 'px');
		bb.setStyle('height', height - borderWidth + 'px');

		if(this._videoButton) {
			this._positionVideoButton();
		}
		if(this._videoBox) {
			this._loadVideo();
		}
		if(!this._loading) {
			if(this._imageInfo) {
				this._load();
			}
			if(this._image) {
				this._positionImage();
			}
		}
	},

	/**
	 * Loads (or reloads) the image or video.
	 *
	 * @method _load
	 * @protected
	 */
	_load: function()
	{
		var loadVideos 		= this.get('loadVideos'),
			showVideoButton = this.get('showVideoButton');

		if(this._isVideo() && loadVideos && !showVideoButton && !('ontouchstart' in window)) {
			this._loadVideo();
		}
		else {
			this._loadImage();
		}
	},

	/**
	 * Loads the appropriate image size if
	 * it is not already loading.
	 *
	 * @method _loadImage
	 * @protected
	 */
	_loadImage: function()
	{
		var url 		= this._getImageURL(),
			loadVideos 	= this.get('loadVideos');

		// Already loading.
		if(url == this._loadingURL) {
			return;
		}

		// New URL to load.
		this._loadingURL = url;

		// Load the new image.
		Y.FL.SlideshowImageLoader.add(
			this.get('loadGroup'),
			this._loadingURL,
			Y.bind(this._loadImageComplete, this),
			this.get('loadPriority')
		);

		// Initial load?
		if(this._loading) {

			if(this._isVideo() && loadVideos) {
				this._insertVideoButton();
			}

			/**
			 * Only fires when a new image is being
			 * loaded, not a different size.
			 *
			 * @event loadStart
			 */
			this.fire('loadStart');
		}
	},

	/**
	 * Fires when the image has finished loading.
	 *
	 * @method _loadImageComplete
	 * @protected
	 */
	_loadImageComplete: function(img)
	{
		var bb            = this.get('boundingBox'),
			showVideoButton = this.get('showVideoButton'),
			showAria        = this.get('root').get('bgslideshow')

		this._image = img;
		this._image.setStyle('visibility', 'hidden');
		this._image.addClass('fl-slideshow-image-img');

		if( showAria ) {
			this._image.set( 'aria-hidden', 'true')
			this._image.set( 'alt', "")
		}
		else {
			this._image.set( 'alt', this._imageInfo.alt )
		}

		// Remove load events.
		this._image.detachAll();

		// Remove previous videos.
		if(this._video && !showVideoButton) {
			this._video.remove();
			this._video = null;
		}

		// Remove the old image.
		bb.all('img').remove();

		// Append the new image.
		bb.append(this._image);

		// Setup, scale and position the new image.
		this._setupImage();
		this._resizeImage();
		this._positionImage();
		this._image.setStyle('visibility', 'visible');

		// Clear the loading url.
		this._loadingURL = null;

		// Finish an initial load?
		if(this._loading) {

			this._loading = false;

			/**
			 * Only fires when a new image is being
			 * loaded, not a different size.
			 *
			 * @event loadComplete
			 */
			this.fire('loadComplete');
		}
	},

	/**
	 * UI setup for the new image.
	 *
	 * @method _setupImage
	 * @protected
	 */
	_setupImage: function()
	{
        var bb = this.get('boundingBox');

		// IE interpolation
		if(typeof this._image._node.style.msInterpolationMode != 'undefined') {
			this._image._node.style.msInterpolationMode = 'bicubic';
		}

		// Protection
		if(this.get('protect')) {
            bb.delegate('contextmenu', this._protectImage, 'img');
            bb.delegate('mousedown', this._protectImage, 'img');
		}
	},

	/**
	 * Fires on contextmenu or mousedown in attempt
	 * to keep the image from being copied.
	 *
	 * @method _protectImage
	 * @return {Boolean} Returns false to prevent the default event.
	 * @protected
	 */
	_protectImage: function(e)
	{
		e.preventDefault();
		return false;
	},

	/**
	 * Resizes the image node.
	 *
	 * @method _resizeImage
	 * @protected
	 */
	_resizeImage: function()
	{
		var borderWidth 		= parseInt(this._image.getComputedStyle('borderTopWidth'), 10) * 2,
			imageWidth			= this._image.get('width'),
			imageHeight			= this._image.get('height'),
			targetWidth			= parseInt(this.get('boundingBox').getComputedStyle('width'), 10),
			targetHeight		= parseInt(this.get('boundingBox').getComputedStyle('height'), 10),
			newWidth 			= 0,
			newHeight  			= 0,
			xScale				= 0,
			yScale				= 0,
			cropHorizontalsOnly = this.get('cropHorizontalsOnly'),
			isHorizontal		= imageHeight > imageWidth,
			noCrop				= false;

		if(this._imageInfo && this.get('checkFilenamesForNoCrop')) {
			noCrop = this._imageInfo.filename.indexOf('nocrop') > -1;
		}

		if(this.get('crop') && !(cropHorizontalsOnly && isHorizontal) && !noCrop) {
			newWidth = targetWidth;
			newHeight = Math.round(imageHeight * targetWidth/imageWidth);

			if(newHeight < targetHeight) {
				newHeight = targetHeight;
				newWidth = Math.round(imageWidth * targetHeight/imageHeight);
			}
		}
		else {
			xScale = imageWidth/targetWidth;
			yScale = imageHeight/targetHeight;

			if (yScale > xScale){
				newWidth = Math.round(imageWidth * (1/yScale));
				newHeight = Math.round(imageHeight * (1/yScale));
			}
			else {
				newWidth = Math.round(imageWidth * (1/xScale));
				newHeight = Math.round(imageHeight * (1/xScale));
			}
		}

		// Don't resize past the original size?
		if(!this.get('crop') && !this.get('upsize') && (newWidth > imageWidth || newHeight > imageHeight)) {
			newWidth = imageWidth;
			newHeight = imageHeight;
		}

		// Compensate for borders.
		newWidth -= borderWidth;
		newHeight -= borderWidth;

		// Resize the image.
		this._image.setStyle('width', newWidth + 'px');
		this._image.setStyle('height', newHeight + 'px');

		// Constrain bounding box to image size.
		if(!this.get('crop') && this.get('constrainWidth')) {
			this.set('width', newWidth + 'px');
		}
		if(!this.get('crop') && this.get('constrainHeight')) {
			this.set('height', newHeight + 'px');
		}
	},

	/**
	 * Positions the image within the bounding box.
	 *
	 * @method _positionImage
	 * @protected
	 */
	_positionImage: function()
	{
		var pos 			= this.get('position').split(' '),
			x 				= pos[0] === '' ? 'center' : pos[0],
			y 				= pos[1] === '' ? 'center' : pos[1],
			newX 			= 0,
			newY			= 0,
			bbWidth			= parseInt(this.get('boundingBox').getComputedStyle('width'), 10),
			bbHeight		= parseInt(this.get('boundingBox').getComputedStyle('height'), 10),
			borderWidth 	= parseInt(this._image.getComputedStyle('borderTopWidth'), 10) * 2,
			imageWidth		= parseInt(this._image.getComputedStyle('width'), 10) + borderWidth,
			imageHeight		= parseInt(this._image.getComputedStyle('height'), 10) + borderWidth;

		if(isNaN(imageWidth) && isNaN(imageHeight)) {
			return;
		}
		if(x == 'left') {
			newX = 0;
		}
		if(x == 'center') {
			newX = 	(bbWidth - imageWidth)/2;
		}
		if(x == 'right') {
			newX = bbWidth - imageWidth;
		}

		if(y == 'top') {
			newY = 0;
		}
		if(y == 'center') {
			newY = (bbHeight - imageHeight)/2;
		}
		if(y == 'bottom') {
			newY = bbHeight - imageHeight;
		}

		this._image.setStyles({
			'left': newX,
			'top': newY
		});
	},

	/**
	 * Gets the appropriate image url based
	 * on the size of the bounding box.
	 *
	 * @method _getImageURL
	 * @return {String} The url to load.
	 * @protected
	 */
	_getImageURL: function()
	{
		var imageWidth 		= 0,
			imageHeight 	= 0,
			size 			= 0,
			targetWidth		= this.get('width'),
			targetHeight	= this.get('height'),
			useThumbSizes	= this.get('useThumbSizes'),
			i 				= this._imageInfo,
			sizes = [
				i.tinyURL 		|| i.thumbURL 	|| i.largeURL,
				i.thumbURL 		|| i.largeURL,
				i.smallURL 		|| i.largeURL,
				i.mediumURL 	|| i.largeURL 	|| i.smallURL,
				i.largeURL 		|| i.mediumURL 	|| i.smallURL,
				i.xlargeURL 	|| i.largeURL 	|| i.mediumURL 	|| i.smallURL,
				i.x2largeURL 	|| i.largeURL 	|| i.mediumURL 	|| i.smallURL,
				i.x3largeURL 	|| i.x2largeURL || i.largeURL 	|| i.mediumURL || i.smallURL
			];

		// Width
		if(useThumbSizes && targetWidth <= 100) {
			imageWidth = 0;
		}
		else if(useThumbSizes && targetWidth <= 150) {
			imageWidth = 1;
		}
		else if(targetWidth <= 400) {
			imageWidth = 2;
		}
		else if(targetWidth >= 400 && targetWidth <= 600) {
			imageWidth = 3;
		}
		else if(targetWidth >= 600 && targetWidth <= 800) {
			imageWidth = 4;
		}
		else if(targetWidth >= 800 && targetWidth <= 1024) {
			imageWidth = 5;
		}
		else if(targetWidth >= 1024 && targetWidth <= 1280) {
			imageWidth = 6;
		}
		else {
			imageWidth = 7;
		}

		// Height
		if(useThumbSizes && targetHeight <= 100) {
			imageHeight = 0;
		}
		else if(useThumbSizes && targetHeight <= 150) {
			imageHeight = 1;
		}
		else if(targetHeight <= 300) {
			imageHeight = 2;
		}
		else if(targetHeight >= 300 && targetHeight <= 450) {
			imageHeight = 3;
		}
		else if(targetHeight >= 450 && targetHeight <= 600) {
			imageHeight = 4;
		}
		else if(targetHeight >= 600 && targetHeight <= 768) {
			imageHeight = 5;
		}
		else if(targetHeight >= 768 && targetHeight <= 960) {
			imageHeight = 6;
		}
		else {
			imageHeight = 7;
		}

		// Get the size number.
		size = Math.max(imageWidth, imageHeight);

		return sizes[size];
	},

	/**
	 * Checks whether this is a video or not.
	 *
	 * @method _isVideo
	 * @protected
	 */
	_isVideo: function()
	{
		if(!this._imageInfo) {
			return false;
		}
		else if(this._imageInfo.format == 'mp4' && this._imageInfo.sourceType == 'smugmug') {
			return true;
		}
		else if(this._imageInfo.iframe !== '') {
			return true;
		}

		return false;
	},

	/**
	 * @method _loadVideo
	 * @protected
	 */
	_loadVideo: function()
	{
		var bb 				= this.get('boundingBox'),
			showVideoButton = this.get('showVideoButton'),
			autoPlay 		= showVideoButton ? true : false;

		// Remove previous videos
		if(this._video) {
			this._video.remove();
			this._video = null;
		}

		// Get the video code
		if(this._imageInfo.format == 'mp4' && this._imageInfo.sourceType == 'smugmug') {
			this._video = this._getSmugMugVideoEmbed(this._imageInfo, autoPlay);
		}
		else if(this._imageInfo.iframe !== '') {
			this._video = this._getIframeVideoEmbed(this._imageInfo, autoPlay);
		}

		// Insert the video
		if(this._videoBox) {
			this._videoBox.one('.fl-slideshow-video-wrap').insert(this._video);
		}
		else {
			bb.all('img').remove();
			bb.append(this._video);
		}

		// Finish an initial load?
		if(this._loading) {
			this._loading = false;
			this.fire('loadComplete');
		}
	},

	/**
	 * @method _insertVideoButton
	 * @protected
	 */
	_insertVideoButton: function()
	{
		var bb 		= this.get('boundingBox'),
			event 	= 'ontouchstart' in window ? 'touchstart' : 'click',
			fallback = this.get('root').get('fallback');

		if(fallback) {
			this._videoButton = Y.Node.create('<a class="fl-slideshow-video-button" role="button" aria-label="insert video" tabindex="0"></a>');
		}
		else {
			this._videoButton = Y.Node.create('<button class="fl-slideshow-video-button fl-content-ui-button" type="button" aria-label="insert video"></button>');
		}
		this._videoButton.on(event, Y.bind(this._showVideoBox, this));
		bb.insert(this._videoButton);
		this._positionVideoButton();
	},

	/**
	 * @method _positionVideoButton
	 * @protected
	 */
	_positionVideoButton: function()
	{
		var bbWidth			= this.get('width'),
			bbHeight		= this.get('height'),
			buttonWidth		= parseInt(this._videoButton.getStyle('width'), 10),
			buttonHeight 	= parseInt(this._videoButton.getStyle('height'), 10);

		this._videoButton.setStyles({
			left: (bbWidth - buttonWidth)/2,
			top: (bbHeight - buttonHeight)/2
		});
	},

	/**
	 * @method _showVideoBox
	 * @protected
	 */
	_showVideoBox: function()
	{
		var root 	= this.get('root'),
			fallback = root.get('fallback'),
			wrap 	  = Y.Node.create('<div class="fl-slideshow-video-wrap"></div>'),
			event 	= 'ontouchstart' in window ? 'touchstart' : 'click',
			tag 		= fallback ? 'a' : 'button',
			attrs		= fallback ? 'role="button"' : 'type="button"',
			classes = fallback ? 'class="fl-slideshow-video-close"' : 'class="fl-slideshow-video-close fl-content-ui-button"',
			close 	= Y.Node.create('<' + [tag, attrs, classes].join(' ') + '></' + tag + '>');

		this._videoBox = Y.Node.create('<div class="fl-slideshow-video"></div>');
		this._videoBox.setStyle('padding', root.get('boundingBox').getStyle('padding'));
		this._videoBox.insert(wrap);
		this._videoBox.insert(close);
		this._videoBox.on(event, Y.bind(this._removeVideoBox, this));
		close.on(event, Y.bind(this._removeVideoBox, this));

		if(typeof YUI.Env.mods['sm-fonticon'] !== 'undefined') {
            close.addClass('sm-fonticon sm-fonticon-XCrossEncircled sm-button-skin-default sm-button-nochrome');
        }

		Y.one('body').insert(this._videoBox);
		this._loadVideo();

		Y.one('body').on('fl-slideshow-image|keydown', this._onKey, this);
	},

	/**
	 * Get the embed code for a SmugMug video.
	 *
	 * @method _getSmugMugVideoEmbed
	 * @param imageInfo {Object} The image info for the embed.
	 * @param autoPlay {Boolean} Whether to auto play videos or not.
	 * @protected
	 */
	_getSmugMugVideoEmbed: function(imageInfo, autoPlay)
	{
		var test		= document.createElement('video'),
			width		= 0,
			mp4 		= '',
			vars		= '',
			code 		= '';

		if(Y.UA.mobile !== null && !!test.canPlayType && test.canPlayType('video/mp4')) {
			width = this.get('width');
			mp4 = 'https://www.smugmug.com/photos/' + imageInfo.id + '_' + imageInfo.key + '-' + width + '.mp4';
			code += '<video width="100%" height="100%" poster="'+ this._getImageURL() +'" controls preload="none"';

			if(autoPlay) {
				code += ' autoplay';
			}

			code += '>';
			code += '<source src="'+ mp4 +'" type="video/mp4" />';
			code += '</video>';
		}
		else {
			vars = 'imageId=' + imageInfo.id;
			vars += '&amp;imageKey=' + imageInfo.key;
			vars += '&amp;albumId=' + imageInfo.albumId;
			vars += '&amp;albumKey=' + imageInfo.albumKey;
			vars += '&amp;apiURL=https://api.smugmug.com/&amp;hostLevel=live&amp;isPro=true';

			if(autoPlay) {
				vars += '&amp;autoPlay=true';
			}
			else {
				vars += '&amp;autoPlay=false';
			}

			code += '<object type="application/x-shockwave-flash" width="100%" height="100%" data="https://cdn.smugmug.com/img/ria/SmugPlayer/2012102601.swf">';
			code += '<param name="movie" value="https://cdn.smugmug.com/img/ria/SmugPlayer/2012102601.swf">';
			code += '<param name="allowFullScreen" value="true">';
			code += '<param name="wmode" value="transparent">';
			code += '<param name="flashVars" value="' + vars + '">';
			code += '<embed src="https://cdn.smugmug.com/img/ria/SmugPlayer/2012102601.swf" flashvars="'+ vars +'" width="100%" height="100%" type="application/x-shockwave-flash" allowfullscreen="true" wmode="transparent">';
			code += '</object>';
		}

		return Y.Node.create(code);
	},

	/**
	 * Get the iframe video embed code.
	 *
	 * @method _getIframeVideoEmbed
	 * @param imageInfo {Object} The image info for the embed.
	 * @param autoPlay {Boolean} Whether to auto play videos or not.
	 * @protected
	 */
	_getIframeVideoEmbed: function(imageInfo, autoPlay)
	{
		var code 	= '<iframe width="100%" height="100%" allowfullscreen ',
			url	 	= imageInfo.iframe;

		if(autoPlay) {
			url += url.indexOf('?') > -1 ? '&autoplay=1' : '?autoplay=1';
		}

		code += 'src="'+ url +'"></iframe>';

		return Y.Node.create(code);
	},

	/**
	 * @method _removeVideoBox
	 * @protected
	 */
	_removeVideoBox: function(e)
	{
		if(typeof e !== 'undefined' && e.target) {
			if(e.target.get('className').indexOf('fl-slideshow-video') < 0) {
				return;
			}
		}

		if(this._videoBox !== null) {
    		this._videoBox.remove();
    		this._videoBox = null;
    		this._video = null;
        }

		Y.one('body').detach('fl-slideshow-image|keydown', this._onKey);
	},

	/**
	 * Keyboard input for the esc button.
	 *
	 * @method _onKey
	 * @protected
	 */
	_onKey: function(e)
	{
		if(e.keyCode == 27) {
			this._removeVideoBox();
			return false;
		}
	}

}, {

	/**
	 * Custom CSS class name for the widget.

	 * @property CSS_PREFIX
	 * @type String
	 * @protected
	 * @static
	 */
	CSS_PREFIX: 'fl-slideshow-image',

	/**
	 * Static property used to define the default attribute configuration of
	 * the Widget.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

		/**
		 * @attribute loadGroup
		 * @type String
		 * @default none
		 */
		loadGroup: {
			value: 'none'
		},

		/**
		 * @attribute loadPriority
		 * @type Boolean
		 * @default false
		 */
		loadPriority: {
			value: false
		},

		/**
		 * Whether to crop the image.
		 *
		 * @attribute crop
		 * @type Boolean
		 * @default false
		 */
		crop: {
			value: false
		},

		/**
		 * Checks whether the filename has nocrop in it or not.
		 * If it does, the image will not be cropped.
		 *
		 * @attribute checkFilenamesForNoCrop
		 * @type Boolean
		 * @default true
		 */
		checkFilenamesForNoCrop: {
			value: true
		},

		/**
		 * Whether to only crop horizontal images or not.
		 *
		 * @attribute cropHorizontalsOnly
		 * @type Boolean
		 * @default false
		 */
		cropHorizontalsOnly: {
			value: false
		},

		/**
		 * The x and y position of the image
		 * within the bounding box.
		 *
		 * @attribute position
		 * @type String
		 * @default center center
		 */
		position: {
			value: 'center center'
		},

		/**
		 * Whether to right click protect the image.
		 *
		 * @attribute protect
		 * @type Boolean
		 * @default true
		 */
		protect: {
			value: true
		},

		/**
		 * Whether to resize the image past
		 * its original width and height.
		 *
		 * @attribute upsize
		 * @type Boolean
		 * @default true
		 */
        upsize: {
			value: true
		},

		/**
		 * Whether to load thumb sizes. Defaults
		 * to false since thumb sizes are square.
		 *
		 * @attribute useThumbSizes
		 * @type Boolean
		 * @default false
		 */
		useThumbSizes: {
			value: false
		},

		/**
		 * Whether to constrain the width of the
		 * bounding box to the width of the image.
		 *
		 * @attribute constrainWidth
		 * @type Boolean
		 * @default false
		 */
		constrainWidth: {
			value: false
		},

		/**
		 * Whether to constrain the height of the
		 * bounding box to the height of the image.
		 *
		 * @attribute constrainHeight
		 * @type Boolean
		 * @default false
		 */
		constrainHeight: {
			value: false
		},

		/**
		 * Whether to load videos or not. The poster
		 * image will be loaded if set to false.
		 *
		 * @attribute loadVideos
		 * @type Boolean
		 * @default true
		 */
		loadVideos: {
			value: true
		},

		/**
		 * Whether to show the video play button or not.
		 * When clicked, videos will be displayed in a
		 * lightbox instead of the slideshow itself.
		 *
		 * @attribute showVideoButton
		 * @type Boolean
		 * @default true
		 */
		showVideoButton: {
			value: true
		}
	}
});

/**
 * A plugin that turns the cursor into a prev or next arrow when
 * it is over the left or right side of the slideshow.
 *
 * @namespace FL
 * @class SlideshowMouseNav
 * @constructor
 * @param config {Object} Configuration object
 * @extends Plugin.Base
 */
Y.namespace('FL').SlideshowMouseNav = Y.Base.create('fl-slideshow-mouse-nav', Y.Plugin.Base, [], {

	/**
	 * @method initializer
	 * @protected
	 */
	initializer: function()
	{
		var trigger = this.get('trigger');

		trigger.on('click', this._triggerClick, this);
		trigger.on('mousemove', this._showArrow, this);
		trigger.on('mouseleave', this._hideArrow, this);
    },

	/**
	 * @method _triggerClick
	 * @protected
	 */
    _triggerClick: function(e)
    {
    	var host 			= this.get('host'),
    		trigger 		= this.get('trigger'),
    		triggerWidth	= parseInt(trigger.getStyle('width'), 10),
    		triggerRegion 	= trigger.get('region'),
    		layerX 			= e.pageX - triggerRegion.left + 5;

    	if(layerX >= triggerWidth/2) {
    		host.nextImage();
    	}
    	else {
    		host.prevImage();
    	}
    },

	/**
	 * @method _showArrow
	 * @protected
	 */
    _showArrow: function(e)
    {
    	var host 			= this.get('host'),
    	    trigger 		= this.get('trigger'),
    		triggerWidth	= parseInt(trigger.getStyle('width'), 10),
    		triggerRegion 	= trigger.get('region'),
    		layerX 			= e.pageX - triggerRegion.left + 5;

    	if(host.albumInfo !== null && host.albumInfo.images.length > 1) {
        	if(layerX >= triggerWidth/2) {
        		trigger.removeClass('fl-slideshow-mouse-nav-prev');
        		trigger.addClass('fl-slideshow-mouse-nav-next');
        	}
        	else {
        		trigger.removeClass('fl-slideshow-mouse-nav-next');
        		trigger.addClass('fl-slideshow-mouse-nav-prev');
        	}
        }
    },

	/**
	 * @method _hideArrow
	 * @protected
	 */
    _hideArrow: function()
    {
    	var trigger = this.get('trigger');

    	trigger.removeClass('fl-slideshow-mouse-nav-next');
    	trigger.removeClass('fl-slideshow-mouse-nav-prev');
    }

},	{

	/**
	 * Namespace for the plugin.
	 *
	 * @property NS
	 * @type String
	 * @protected
	 * @static
	 */
	NS: 'mouseNav',

	/**
	 * Static property used to define the default attribute configuration of
	 * the Plugin.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

		/**
		 * A Node that triggers the arrows.
		 *
		 * @attribute trigger
		 * @type Node
		 * @default null
		 */
		trigger: {
			value: null
		}
	}
});

/**
 * Ken Burns effect for slideshow images.
 *
 * @namespace FL
 * @class SlideshowKenBurns
 * @constructor
 * @param config {Object} Configuration object
 * @extends Base
 */
Y.namespace('FL').SlideshowKenBurns = Y.Base.create('fl-slideshow-ken-burns', Y.Base, [], {

	/**
	 * Runs the Ken Burns effect.
	 *
	 * @method run
	 */
	run: function()
	{
        var imageNode = null,
            transform = null;

		if(Y.FL.Utils.cssSupport('transform')) {

            // Image node
            imageNode = this.get('image').one('img');

            // Transform object
            transform = this._getTransform();

            // Apply the start transform
            imageNode.setStyles({
                '-webkit-transform-origin': transform.origin,
                '-moz-transform-origin': transform.origin,
                '-ms-transform-origin': transform.origin,
                'transform-origin': transform.origin,
                'transform': transform.start
            });

            // Transition to the end transform
            imageNode.transition({
                easing: 'ease-out',
                duration : this.get('duration'),
                'transform' : transform.end
            });
        }
	},

	/**
	 * @method _getTransform
	 * @protected
	 */
	_getTransform: function()
	{
        var zoom            = this.get('zoom'),
            image           = this.get('image'),
            i               = 0,
            zoomDirection   = null,
            transform       = null;

        // Random zoom direction
        i = Math.floor(Math.random() * Y.FL.SlideshowKenBurns.ZOOM_DIRECTIONS.length);
        zoomDirection = Y.FL.SlideshowKenBurns.ZOOM_DIRECTIONS[i];

        // Random transform
        i = Math.floor(Math.random() * Y.FL.SlideshowKenBurns.TRANSFORMS.length);
        transform = Y.FL.SlideshowKenBurns.TRANSFORMS[i];

        // Get the start and end transforms
        if(!image.hasClass('fl-slideshow-image-cropped') && zoomDirection == 'in') {
            i = Math.floor(Math.random() * 2);
            transform.start = i === 0 ? 'scale(1) translate(100px, 0)' : 'scale(1) translate(-100px, 0)';
            transform.end = 'scale(' + zoom + ') translate(0, 0)';
            transform.origin = 'center center';
        }
        else if(zoomDirection == 'out') {
            transform.start = 'scale(' + zoom + ') ' + transform.translate;
            transform.end = 'scale(1) translate(0, 0)';
        }
        else {
            transform.start = 'scale(1) translate(0, 0)';
            transform.end = 'scale(' + zoom + ') ' + transform.translate;
        }

        return transform;
	}

}, {

	/**
	 * Static property used to define the default attribute configuration of
	 * the Widget.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

		/**
		 * An instance of FL.Slideshow image to apply the
		 * Ken Burns effect on.
		 *
		 * @attribute image
		 * @type FL.Slideshow
		 * @default null
		 */
		image: {
			value: null
		},

		/**
		 * The amount to zoom the image. Zooming
		 * in our out is done randomly by this class.
		 *
		 * @attribute scale
		 * @type Number
		 * @default 1.2
		 */
		zoom: {
		  value: 1.2
		},

		/**
		 * The duration of the effect in seconds.
		 *
		 * @attribute duration
		 * @type Number
		 * @default 2
		 */
		duration: {
			value: 2
		}
	},

	/**
	 * The zoom directions that can be applied to an image.
	 *
	 * @property ZOOM_DIRECTIONS
	 * @type Object
	 * @readOnly
	 * @protected
	 * @static
	 */
	ZOOM_DIRECTIONS: [
        'in',
        'out'
    ],

	/**
	 * The types of transforms that can be applied to an image.
	 *
	 * @property TRANSFORMS
	 * @type Object
	 * @readOnly
	 * @protected
	 * @static
	 */
    TRANSFORMS: [
        {
            origin    : 'left top',
            translate : 'translate(-30px, -15px)'
        },{
            origin    : 'left center',
            translate : 'translate(-30px, 0)'
        },{
            origin    : 'left bottom',
            translate : 'translate(-30px, 15px)'
        },{
            origin    : 'right top',
            translate : 'translate(30px, -15px)'
        },{
            origin    : 'right center',
            translate : 'translate(30px, 0)'
        },{
            origin    : 'right bottom',
            translate : 'translate(30px, 15px)'
        }
    ]
});

/**
 * Navigation buttons widget for controlling a slideshow instance
 * and its child widgets.
 *
 * @namespace FL
 * @class SlideshowNav
 * @constructor
 * @param config {Object} Configuration object
 * @extends Widget
 */
Y.namespace('FL').SlideshowNav = Y.Base.create('fl-slideshow-nav', Y.Widget, [Y.WidgetChild], {

	/**
	 * An object containing the anchor nodes for all buttons.
	 *
	 * @property _buttons
	 * @type Object
	 * @default null
	 * @protected
	 */
	_buttons: null,

	/**
	 * An div node containing the anchor nodes for the main buttons.
	 *
	 * @property _buttonsContainer
	 * @type Object
	 * @default null
	 * @protected
	 */
	_buttonsContainer: null,

	/**
	 * An div node containing the anchor nodes for the left buttons.
	 *
	 * @property _buttonsLeftContainer
	 * @type Object
	 * @default null
	 * @protected
	 */
	_buttonsLeftContainer: null,

	/**
	 * An div node containing the anchor nodes for the right buttons.
	 *
	 * @property _buttonsRightContainer
	 * @type Object
	 * @default null
	 * @protected
	 */
	_buttonsRightContainer: null,

	/**
	 * Property map for rendering SmugMug font icons.
	 *
	 * @property _fontIcons
	 * @type Object
	 * @protected
	 */
	_fontIcons: {
        buy: 'Cart',
        caption: 'InfoEncircled',
        close: 'XCrossEncircled',
        fullscreen: 'ScreenExpand',
        next: 'ArrowRight',
        nextPage: 'ArrowRight',
        pause: 'PlayerPause',
        play: 'PlayerPlay',
        prev: 'ArrowLeft',
        prevPage: 'ArrowLeft',
        social: 'Heart',
        thumbs: 'ViewThumbGrid'
    },

	/**
	 * The default content template for the nav
	 * inherited from Y.Widget. Set to null since
	 * only the bounding box is needed.
	 *
	 * @property CONTENT_TEMPLATE
	 * @type String
	 * @default null
	 * @protected
	 */
	CONTENT_TEMPLATE: null,

	/**
	 * Renders the buttons.
	 *
	 * @method renderUI
	 * @protected
	 */
	renderUI: function()
	{
		this._renderContainers();
		this._renderButtons();
		this._renderFontIcons();
	},

	/**
	 * Binds events to the root slideshow for each button.
	 *
	 * @method bindUI
	 * @protected
	 */
	bindUI: function()
	{
		var root 		= this.get('root'),
			id 			= this.get('id');

		if(this._buttons.prev) {
			this._buttons.prev.on('click', root.prevImage, root);
		}
		if(this._buttons.next) {
			this._buttons.next.on('click', root.nextImage, root);
		}
		if(this._buttons.play) {
			this._buttons.play.on('click', this._playClicked, this);
			root.on(id + '|played', this._showPauseButton, this);
			root.on(id + '|paused', this._showPlayButton, this);

			if(root._playing) {
				this._showPauseButton();
			}
			else {
				this._showPlayButton();
			}
		}
		if(this._buttons.buy) {

			root.on(id + '|albumLoadComplete', this._updateBuy, this);

			if(root.albumInfo !== null) {
				this._updateBuy();
			}
		}
		if(this._buttons.count) {
			root.on(id + '|imageLoadComplete', this._updateCount, this);
		}
		if(this._buttons.thumbs) {
			this._buttons.thumbs.on('click', root._toggleThumbs, root);
		}
		if(this._buttons.caption) {
			root.on(id + '|imageLoadComplete', this._updateCaption, this);
			this._updateCaption();
		}
		if(this._buttons.social) {
			this._buttons.social.on('click', root._toggleSocial, root);
		}
		if(this._buttons.fullscreen && root.fullscreen) {
			this._buttons.fullscreen.on('click', root.fullscreen.toggle, root.fullscreen);
		}
		if(this._buttons.close) {
			this._buttons.close.on('click', root.hide, root);
		}
	},

	/**
	 * @method destructor
	 * @protected
	 */
	destructor: function()
	{
		var root 	= this.get('root'),
			id 		= this.get('id');

		root.detach(id + '|*');
	},

	/**
	 * Renders the button left, right and main button containers.
	 *
	 * @method _renderContainers
	 * @protected
	 */
	_renderContainers: function()
	{
		var cb				= this.get('contentBox'),
			buttonsLeft		= this.get('buttonsLeft'),
			buttonsRight	= this.get('buttonsRight');

		this._buttonsContainer = Y.Node.create('<div></div>');
		this._buttonsContainer.addClass('fl-slideshow-nav-buttons');
		cb.appendChild(this._buttonsContainer);

		if(buttonsLeft.length > 0) {
			this._buttonsLeftContainer = Y.Node.create('<div></div>');
			this._buttonsLeftContainer.addClass('fl-slideshow-nav-buttons-left');
			cb.appendChild(this._buttonsLeftContainer);
		}
		if(buttonsRight.length > 0) {
			this._buttonsRightContainer = Y.Node.create('<div></div>');
			this._buttonsRightContainer.addClass('fl-slideshow-nav-buttons-right');
			cb.appendChild(this._buttonsRightContainer);
		}
	},

	/**
	 * Renders the buttons based on the buttons array
	 * passed in the configuration object.
	 *
	 * @method _renderButtons
	 * @protected
	 */
	_renderButtons: function()
	{
		var name 		= '',
			fallback 	= this.get('root').get('fallback'),
			tag 		= fallback ? 'a' : 'button',
			attrs 	= fallback ? 'role="button" tabindex="0"' : 'type="button"',
			i 			= 0,
			k			= 0,
			b = [
			{
				names: this.get('buttons'),
				container: this._buttonsContainer
			},
			{
				names: this.get('buttonsLeft'),
				container: this._buttonsLeftContainer
			},
			{
				names: this.get('buttonsRight'),
				container: this._buttonsRightContainer
			}
		];

		this._buttons = {};

		for( ; i < b.length; i++) {
			for(k = 0; k < b[i].names.length; k++) {

				name = b[i].names[k];

				if(name.indexOf('count') > -1) {
					this._buttons[name] = Y.Node.create('<span></span>');
					this._updateCount();
				}
				else {
					this._buttons[name] = Y.Node.create('<' + tag + ' ' + attrs + '></' + tag + '>');
				}

				if(name.indexOf('buy') > -1) {
					this._buttons[name].setStyle('display', 'none');
				}

				this._buttons[name].set('name', name);
				this._buttons[name].set('aria-label', name);
				this._buttons[name].addClass('fl-slideshow-nav-' + name + (!fallback ? ' fl-content-ui-button' : ''));
				b[i].container.appendChild(this._buttons[name]);
			}
		}
	},

	/**
	 * Renders SmugMug font icons for each button.
	 *
	 * @method _renderFontIcons
	 * @protected
	 */
	_renderFontIcons: function()
	{
        var name = null;

        if(this.get('useFontIcons') && typeof YUI.Env.mods['sm-fonticon'] !== 'undefined') {
            for(name in this._buttons) {
                if(typeof this._buttons[name] !== 'undefined' && typeof this._fontIcons[name] !== 'undefined') {
                    this._buttons[name].addClass('sm-fonticon-' + this._fontIcons[name]);
                    this._buttons[name].addClass('sm-fonticon sm-button-skin-default sm-button-nochrome');
                }
                else if(name.indexOf('count') > -1) {
                    this._buttons[name].addClass('fonticons-enabled');
                }
            }
        }
	},

	/**
	 * Updates the image count.
	 *
	 * @method _updateCount
	 * @protected
	 */
	_updateCount: function()
	{
		var html 		= '',
			countText	= Y.FL.SlideshowNav.COUNT_TEXT,
			current 	= 1,
			total 		= 1;

		if(this.get('root').albumInfo) {
			current 	= this.get('root').imageInfo.index + 1;
			total 		= this.get('root').albumInfo.images.length;
		}

		html = countText.replace('{current}', current).replace('{total}', total);
		this._buttons.count.set('innerHTML', html);
	},

	/**
	 * Shows the caption button if the current image
	 * has a caption, hides it if the image does not
	 * have a caption.
	 *
	 * @method _updateCaption
	 * @protected
	 */
	_updateCaption: function()
	{
		var root		= this.get('root'),
			imageInfo 	= root.imageInfo;

		if(imageInfo && imageInfo.caption === '') {
			root.caption.slideshowOverlay.enable();
			root.caption.slideshowOverlay.hide();
			this._buttons.caption.detach('click');
			this._buttons.caption.addClass('fl-slideshow-nav-caption-disabled');
		}
		else {
			this._buttons.caption.on('click', root._toggleCaption, root);
			this._buttons.caption.removeClass('fl-slideshow-nav-caption-disabled');
		}
	},

	/**
	 * Checks if buying has been enabled for the current album.
	 *
	 * @method _updateBuy
	 * @protected
	 */
	_updateBuy: function()
	{
		var sm 				= null,
			root			= this.get('root'),
			rootSource 		= root.get('source')[root.albumIndex],
			albumIndex		= root.albumIndex,
			source 			= root.get('source')[albumIndex];

		if(rootSource && rootSource.type == 'smugmug') {
			if(typeof root.albumInfo.printable !== 'undefined') {
				this._updateBuyComplete();
			}
			else {
				sm = new Y.FL.SmugMugAPI();
				sm.addParam('method', 'smugmug.albums.getInfo');
				sm.addParam('AlbumID', source.id);
				sm.addParam('AlbumKey', source.key);
				sm.on('complete', this._updateBuyComplete, this);
				sm.request();
			}
		}
	},

	/**
	 * Shows the buy button and updates the buy url
	 * if buying has been enabled.
	 *
	 * @method _updateBuyComplete
	 * @param e {Object} The custom event object passed to this function.
	 * @protected
	 */
	_updateBuyComplete: function(e)
	{
		var root		= this.get('root'),
			printable 	= typeof e == 'undefined' ? root.albumInfo.printable : e.Album.Printable,
			link 		= root.albumInfo.link;

		if(printable) {
			root.albumInfo.printable = true;
			this._buttons.buy.set('href', 'https://secure.smugmug.com/cart/batchadd/?url=' + encodeURIComponent(link));
			this._buttons.buy.setStyle('display', 'inline-block');
		}
		else {
			root.albumInfo.printable = false;
			this._buttons.buy.setStyle('display', 'none');
		}

		this.fire('resize');
	},

	/**
	 * Pauses the slideshow if it is playing and
	 * plays the slideshow if it is paused.
	 *
	 * @method _playClicked
	 * @protected
	 */
	_playClicked: function()
	{
		var root = this.get('root');

		if(root._playing) {
			root.pause();
		}
		else {
			root.play();
		}
	},

	/**
	 * Toggles the button class for the play button
	 * so pause is hidden and play is shown.
	 *
	 * @method _showPlayButton
	 * @protected
	 */
	_showPlayButton: function()
	{
		this._buttons.play.removeClass('fl-slideshow-nav-pause');
		this._buttons.play.addClass('fl-slideshow-nav-play');

		if(this.get('useFontIcons') && typeof YUI.Env.mods['sm-fonticon'] !== 'undefined') {
            this._buttons.play.removeClass('sm-fonticon-PlayerPause');
            this._buttons.play.addClass('sm-fonticon-PlayerPlay');
		}
	},

	/**
	 * Toggles the button class for the play button
	 * so pause is shown and play is hidden.
	 *
	 * @method _showPauseButton
	 * @protected
	 */
	_showPauseButton: function()
	{
		this._buttons.play.removeClass('fl-slideshow-nav-play');
		this._buttons.play.addClass('fl-slideshow-nav-pause');

		if(this.get('useFontIcons') && typeof YUI.Env.mods['sm-fonticon'] !== 'undefined') {
            this._buttons.play.removeClass('sm-fonticon-PlayerPlay');
            this._buttons.play.addClass('sm-fonticon-PlayerPause');
		}
	}

}, {

	/**
	 * Custom CSS class name for the widget.
	 *
	 * @property CSS_PREFIX
	 * @type String
	 * @protected
	 * @static
	 */
	CSS_PREFIX: 'fl-slideshow-nav',

	/**
	 * Static string used for displaying the image count. Use {current}
	 * for the current image and {total} for the total number of images.
	 * Those placeholders will be replaced when the count node is created.
	 *
	 * @property COUNT_TEXT
	 * @type String
	 * @protected
	 * @static
	 */
	COUNT_TEXT: '{current} of {total}',

	/**
	 * Static property used to define the default attribute configuration of
	 * the Widget.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

		/**
		 * An array of button names that is used to render the main buttons.
		 *
		 * @attribute buttons
		 * @type Array
		 * @default []
		 * @writeOnce
		 */
		buttons: {
			value: [],
			writeOnce: true
		},

		/**
		 * An array of button names that is used to render the left buttons.
		 *
		 * @attribute buttonsLeft
		 * @type Array
		 * @default []
		 * @writeOnce
		 */
		buttonsLeft: {
			value: [],
			writeOnce: true
		},

		/**
		 * An array of button names that is used to render the right buttons.
		 *
		 * @attribute buttonsRight
		 * @type Array
		 * @default []
		 * @writeOnce
		 */
		buttonsRight: {
			value: [],
			writeOnce: true
		},

		/**
		 * Whether to use font icons when available.
		 *
		 * @attribute useFontIcons
		 * @type Boolean
		 * @default true
		 * @writeOnce
		 */
		useFontIcons: {
			value: true,
			writeOnce: true
		}
	}
});

/**
 * A plugin for overlaying widgets in a slideshow
 * with specialized show and hide functionality.
 *
 * @namespace FL
 * @class SlideshowOverlay
 * @constructor
 * @param config {Object} Configuration object
 * @extends Plugin.Base
 */
Y.namespace('FL').SlideshowOverlay = Y.Base.create('fl-slideshow-overlay', Y.Plugin.Base, [], {

	/**
	 * Flag for whether the mouse has entered
	 * the host's bounding box.
	 *
	 * @property _focus
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_focus: false,

	/**
	 * Flag for whether the host's bounding box is visible.
	 *
	 * @property _visible
	 * @type Boolean
	 * @default true
	 * @protected
	 */
	_visible: true,

	/**
	 * Flag for whether show and hide functionality
	 * has been disabled.
	 *
	 * @property _disabled
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_disabled: false,

	/**
	 * An object containing properties for the show transition.
	 *
	 * @property _showProps
	 * @type Object
	 * @protected
	 */
	_showProps: {
	    duration: 0.5,
	    easing: 'ease-out',
	    opacity: 1
	},

	/**
	 * An object containing properties for the hide transition.
	 *
	 * @property _hideProps
	 * @type Object
	 * @protected
	 */
	_hideProps: {
	    duration: 0.5,
	    easing: 'ease-out',
	    opacity: 0
	},

	/**
	 * A timer object for delaying the hide transition.
	 *
	 * @property _hideTimer
	 * @type Object
	 * @default null
	 * @protected
	 */
	_hideTimer: null,

	/**
	 * @method initializer
	 * @protected
	 */
	initializer: function()
	{
		var bb = this.get('host').get('boundingBox');

		this.afterHostEvent('render', this._initFocus);
		this.afterHostEvent('render', this._initVisibility);

		if(this.get('closeButton')) {
			this._initCloseButton();
		}

		bb.addClass('fl-slideshow-overlay');
    },

	/**
	 * @method destructor
	 * @protected
	 */
	destructor: function()
	{
		this._hideTimerCancel();
	},

	/**
	 * Binds the mouseenter and mouseleave events for setting focus.
	 *
	 * @method _initFocus
	 * @protected
	 */
	_initFocus: function()
	{
		var bb = this.get('host').get('boundingBox');
		bb.on('mouseenter', Y.bind(this._setFocusOnMouseenter, this));
		bb.on('mouseleave', Y.bind(this._setFocusOnMouseleave, this));
	},

	/**
	 * Sets the initial visibility of the host's bounding box.
	 *
	 * @method _initVisibility
	 * @protected
	 */
	_initVisibility: function()
	{
		var bb 			= this.get('host').get('boundingBox'),
			hideStyle 	= this.get('hideStyle');

		if(!this.get('visible')) {

			if(hideStyle == 'display') {
				bb.setStyle('display', 'none');
			}
			else if(hideStyle == 'left') {
				bb.setStyle('left', '-99999px');
			}

			bb.setStyle('opacity', '0');
			this._visible = false;
		}
	},

	/**
	 * Creates and inserts the close button.
	 *
	 * @method _initCloseButton
	 * @protected
	 */
	_initCloseButton: function()
	{
		var bb 			= this.get('host').get('boundingBox'),
			fallback 	= this.get('host').get('root').get('fallback'),
			closeButton = null;

		if(fallback) {
			closeButton = Y.Node.create('<a class="fl-slideshow-overlay-close" role="button" aria-label="close overlay" tabindex="0"></a>');
		}
		else {
			closeButton = Y.Node.create('<button class="fl-slideshow-overlay-close fl-content-ui-button" type="button" aria-label="close overlay"></button>');
		}
		closeButton.on('click', Y.bind(this._closeButtonClick, this));

		if(typeof YUI.Env.mods['sm-fonticon'] !== 'undefined') {
            closeButton.addClass('sm-fonticon sm-fonticon-XCrossEncircled sm-button-skin-default sm-button-nochrome');
        }

		bb.insert(closeButton);
	},

	/**
	 * Hides the overlay when the close button is clicked.
	 *
	 * @method _closeButtonClick
	 * @protected
	 */
	_closeButtonClick: function()
	{
		var bb = this.get('host').get('boundingBox');
		bb.transition(this._hideProps, Y.bind(this._hideComplete, this));
	},

	/**
	 * Sets the focus flag to true.
	 *
	 * @method _setFocusOnMouseenter
	 * @protected
	 */
	_setFocusOnMouseenter: function()
	{
		this._focus = true;
	},

	/**
	 * Sets the focus flag to false.
	 *
	 * @method _setFocusOnMouseleave
	 * @protected
	 */
	_setFocusOnMouseleave: function()
	{
		this._focus = false;
	},

	/**
	 * Disables show and hide functionality.
	 *
	 * @method disable
	 * @public
	 */
	disable: function()
	{
		this._disabled = true;
	},

	/**
	 * Enables show and hide functionality.
	 *
	 * @method enable
	 * @public
	 */
	enable: function()
	{
		this._disabled = false;
	},

	/**
	 * Shows the host's bounding box with a fade in transition.
	 *
	 * @method show
	 * @public
	 */
	show: function()
	{
		var bb 			= this.get('host').get('boundingBox'),
			hideStyle 	= this.get('hideStyle');

		if(this._disabled) {
			return;
		}

		if(hideStyle == 'display') {
			bb.setStyle('display', 'block');
		}
		else if(hideStyle == 'left') {
			bb.setStyle('left', 'auto');
		}

		bb.transition(this._showProps, Y.bind(this._showComplete, this));

		/**
		 * @event hideStart
		 */
		this.fire('showStart');
	},

	/**
	 * @method _showComplete
	 * @protected
	 */
	_showComplete: function()
	{
		this._visible = true;
		this.hideWithTimer();

		/**
		 * @event showComplete
		 */
		this.fire('showComplete');
	},

	/**
	 * Hides the host's bounding box with a fade out transition.
	 *
	 * @method hide
	 * @public
	 */
	hide: function()
	{
		if(this._focus || this._disabled) {
			return;
		}

		var bb = this.get('host').get('boundingBox');
		bb.transition(this._hideProps, Y.bind(this._hideComplete, this));

		/**
		 * @event hideStart
		 */
		this.fire('hideStart');
	},

	/**
	 * Hides the host's bounding box with a fade out transition
	 * after a timer completes.
	 *
	 * @method hideWithTimer
	 * @public
	 */
	hideWithTimer: function()
	{
		this._hideTimerCancel();
		this._hideTimer = Y.later(this.get('hideDelay'), this, this.hide);
	},

	/**
	 * Cancels the hide timer.
	 *
	 * @method _hideTimerCancel
	 * @protected
	 */
	_hideTimerCancel: function()
	{
		if(this._hideTimer) {
			this._hideTimer.cancel();
			this._hideTimer = null;
		}
	},

	/**
	 * @method _hideComplete
	 * @protected
	 */
	_hideComplete: function()
	{
		var bb 			= this.get('host').get('boundingBox'),
			hideStyle 	= this.get('hideStyle');

		if(hideStyle == 'display') {
			bb.setStyle('display', 'none');
		}
		else if(hideStyle == 'left') {
			bb.setStyle('left', '-99999px');
		}

		this._visible = false;

		/**
		 * @event hideComplete
		 */
		this.fire('hideComplete');
	}

},	{

	/**
	 * Namespace for the plugin.
	 *
	 * @property NS
	 * @type String
	 * @protected
	 * @static
	 */
	NS: 'slideshowOverlay',

	/**
	 * Static property used to define the default attribute configuration of
	 * the plugin.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

		/**
		 * Whether to use the close button or not.
		 *
		 * @attribute closeButton
		 * @type Boolean
		 * @default false
		 * @writeOnce
		 */
		closeButton: {
			value: false,
			writeOnce: true
		},

		/**
		 * The time to wait before hiding the host's bounding box.
		 * Measured in milliseconds.
		 *
		 * @attribute hideDelay
		 * @type Number
		 * @default 3000
		 * @writeOnce
		 */
		hideDelay: {
			value: 3000,
			writeOnce: true
		},

		/**
		 * The style to use for hiding the image. Possible
		 * values are display and left.
		 *
		 * @attribute hideStyle
		 * @type String
		 * @default display
		 * @writeOnce
		 */
		hideStyle: {
			value: 'display',
			writeOnce: true
		},

		/**
		 * Sets the initial visibility of the host's boudning box.
		 *
		 * @attribute visible
		 * @type Boolean
		 * @default true
		 * @writeOnce
		 */
		visible: {
			value: true,
			writeOnce: true
		}
	}
});

/**
 * Social buttons widget used in slideshows.
 *
 * @namespace FL
 * @class SlideshowSocial
 * @constructor
 * @param config {Object} Configuration object
 * @extends Widget
 */

Y.namespace('FL').SlideshowSocial = Y.Base.create('fl-slideshow-social', Y.Widget, [Y.WidgetChild], {

	/**
	 * An object containing the social button nodes.
	 *
	 * @property _buttons
	 * @type Object
	 * @default null
	 * @protected
	 */
	_buttons: null,

	/**
	 * @method renderUI
	 * @protected
	 */
	renderUI: function()
	{
		this._buttons = {};
	},

	/**
	 * @method bindUI
	 * @protected
	 */
	bindUI: function()
	{
		var root = this.get('root');

		if(root.get('likeButtonEnabled')) {
			root.on('imageLoadComplete', Y.bind(this._updateLikeButton, this));
		}
		if(root.get('tweetButtonEnabled')) {
			root.on('imageLoadComplete', Y.bind(this._updateTweetButton, this));
		}
		if(root.get('pinterestButtonEnabled')) {
			root.on('imageLoadComplete', Y.bind(this._updatePinterestButton, this));
		}
	},

	/**
	 * @method _updateLikeButton
	 * @protected
	 */
	_updateLikeButton: function()
	{
		var src				= null,
			cb				= this.get('contentBox'),
			root			= this.get('root'),
			albumIndex		= root.albumIndex,
			rootSource 		= root.get('source')[albumIndex],
			imageInfo 		= root.imageInfo;

		if(this._buttons.like) {
			this._buttons.like.remove();
			this._buttons.like = null;
		}

		if(rootSource.type == 'smugmug') {
			src = 'https://www.facebook.com/plugins/like.php?';
			src += 'href=' + 'https://www.smugmug.com/services/graph/gallery/';
			src += rootSource.id + '_' + rootSource.key +'/' + imageInfo.id + '_' + imageInfo.key;
		}
		else {
			src = 'https://www.facebook.com/plugins/like.php?';
			src += 'href=' + encodeURIComponent(imageInfo.largeURL);
		}

		src += '&send=false';
		src += '&layout=button_count';
		src += '&width=90';
		src += '&show_faces=false';
		src += '&action=like';
		src += '&colorscheme=light';
		src += '&height=21';

		this._buttons.like = Y.Node.create('<iframe src="'+ src +'" scrolling="no" allowTransparency="true"></iframe>');

		this._buttons.like.setStyles({
			overflow: 'hidden',
			width: '90px',
			height: '21px'
		});

		cb.appendChild(this._buttons.like);
	},

	/**
	 * @method _updateTweetButton
	 * @protected
	 */
	_updateTweetButton: function()
	{
		var src			= null,
			imageInfo 	= this.get('root').imageInfo,
			cb			= this.get('contentBox');

		if(this._buttons.tweet) {
			this._buttons.tweet.remove();
			this._buttons.tweet = null;
		}

		src = 'https://platform.twitter.com/widgets/tweet_button.html?';
		src += 'url=' + encodeURIComponent(imageInfo.largeURL);
		src += '&count=none';

		this._buttons.tweet = Y.Node.create('<iframe src="'+ src +'" scrolling="no" allowTransparency="true"></iframe>');

		this._buttons.tweet.setStyles({
			overflow: 'hidden',
			width: '90px',
			height: '21px'
		});

		cb.appendChild(this._buttons.tweet);
	},

	/**
	 * @method _updatePinterestButton
	 * @protected
	 */
	_updatePinterestButton: function()
	{
		var href		= 'https://pinterest.com/pin/create/button/',
			imageInfo 	= this.get('root').imageInfo,
			cb			= this.get('contentBox');

		if(this._buttons.pin) {
			this._buttons.pin.remove();
			this._buttons.pin = null;
		}

		href += '?url=' + encodeURIComponent(window.location.href);
		href += '&media='+ encodeURIComponent(imageInfo.mediumURL);
		href += '&description='+ encodeURIComponent(imageInfo.caption);

		this._buttons.pin = Y.Node.create('<a></a>');
		this._buttons.pin.setAttribute('data-pin-config', 'none');
		this._buttons.pin.setAttribute('data-pin-do', 'buttonPin');
		this._buttons.pin.setAttribute('href', href);
		this._buttons.pin.setAttribute('target', '_blank');
		this._buttons.pin.set('innerHTML', '<img src="https://assets.pinterest.com/images/pidgets/pin_it_button.png" border="0" />');

		cb.appendChild(this._buttons.pin);
	}

}, {

	/**
	 * Custom CSS class name for the widget.
	 *
	 * @property CSS_PREFIX
	 * @type String
	 * @protected
	 * @static
	 */
	CSS_PREFIX: 'fl-slideshow-social',

	/**
	 * Static property used to define the default attribute configuration of
	 * the Widget.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

	}
});

/**
 * Creates a grid of FL.SlideshowImage instances.
 *
 * @namespace FL
 * @class SlideshowThumbs
 * @constructor
 * @param config {Object} Configuration object
 * @extends Widget
 */
Y.namespace('FL').SlideshowThumbs = Y.Base.create('fl-slideshow-thumbs', Y.Widget, [Y.WidgetParent, Y.WidgetChild], {

	/**
	 * A div node used to hide the overflow when
	 * transitioning between pages.
	 *
	 * @property _clipBox
	 * @type Object
	 * @default null
	 * @protected
	 */
	_clipBox: null,

	/**
	 * A div node used to hold the pages.
	 *
	 * @property _pagesBox
	 * @type Object
	 * @default null
	 * @protected
	 */
	_pagesBox: null,

	/**
	 * A reference to the active page div node. Holds a grid
	 * of FL.SlideshowImage instances.
	 *
	 * @property _activePageBox
	 * @type Object
	 * @default null
	 * @protected
	 */
	_activePageBox: null,

	/**
	 * The index of the active page of thumbs.
	 *
	 * @property _activePageIndex
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_activePageIndex: 0,

	/**
	 * A reference to the next page div node. Holds a grid
	 * of FL.SlideshowImage instances.
	 *
	 * @property _nextPageBox
	 * @type Object
	 * @default null
	 * @protected
	 */
	_nextPageBox: null,

	/**
	 * An array of FL.SlideshowImage instances in the active page.
	 *
	 * @property _activeImages
	 * @type Array
	 * @default null
	 * @protected
	 */
	_activeImages: null,

	/**
	 * An array of FL.SlideshowImage instances used to
	 * preload the next page of images.
	 *
	 * @property _nextImages
	 * @type Array
	 * @default null
	 * @protected
	 */
	_nextImages: null,

	/**
	 * An array of FL.SlideshowImage instances used to
	 * preload the previous page of images.
	 *
	 * @property _prevImages
	 * @type Array
	 * @default null
	 * @protected
	 */
	_prevImages: null,

	/**
	 * An instance of FL.SlideshowNav used for the left nav.
	 *
	 * @property _leftNav
	 * @type Object
	 * @default null
	 * @protected
	 */
	_leftNav: null,

	/**
	 * An instance of FL.SlideshowNav used for the right nav.
	 *
	 * @property _rightNav
	 * @type Object
	 * @default null
	 * @protected
	 */
	_rightNav: null,

	/**
	 * An instance of FL.SlideshowNav used for the top nav.
	 *
	 * @property _topNav
	 * @type Object
	 * @default null
	 * @protected
	 */
	_topNav: null,

	/**
	 * An instance of FL.SlideshowNav used for the bottom nav.
	 *
	 * @property _bottomNav
	 * @type Object
	 * @default null
	 * @protected
	 */
	_bottomNav: null,

	/**
	 * Height of the bounding box.
	 *
	 * @property _bbHeight
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_bbHeight: 0,

	/**
	 * Width of the bounding box.
	 *
	 * @property _bbWidth
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_bbWidth: 0,

	/**
	 * Width of the content box.
	 *
	 * @property _cbWidth
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_cbWidth: 0,

	/**
	 * Left margin of the clip box.
	 *
	 * @property _clipBoxMarginLeft
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_clipBoxMarginLeft: 0,

	/**
	 * Top position of the clip box.
	 *
	 * @property _clipBoxTop
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_clipBoxTop: 0,

	/**
	 * The number of columns per page.
	 *
	 * @property _colsPerPage
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_colsPerPage: 0,

	/**
	 * The number of rows per page.
	 *
	 * @property _rowsPerPage
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_rowsPerPage: 0,

	/**
	 * The number of images per page.
	 *
	 * @property _imagesPerPage
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_imagesPerPage: 0,

	/**
	 * The number of pages.
	 *
	 * @property _numPages
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_numPages: 0,

	/**
	 * Height of the pages.
	 *
	 * @property _pageHeight
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_pageHeight: 0,

	/**
	 * Width of the pages.
	 *
	 * @property _pageWidth
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_pageWidth: 0,

	/**
	 * The horizontal spacing between thumbs.
	 *
	 * @property _horizontalSpacing
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_horizontalSpacing: 0,

	/**
	 * The vertical spacing between thumbs.
	 *
	 * @property _verticalSpacing
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_verticalSpacing: 0,

	/**
	 * Width of the left nav.
	 *
	 * @property _leftNavWidth
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_leftNavWidth: 0,

	/**
	 * Width of the right nav.
	 *
	 * @property _rightNavWidth
	 * @type Number
	 * @default 0
	 * @protected
	 */
	_rightNavWidth: 0,

	/**
	 * An instance of FL.SlideshowTransition for the current transition.
	 *
	 * @property _transition
	 * @type FL.SlideshowTransition
	 * @default null
	 * @protected
	 */
	_transition: null,

	/**
	 * Whether the pages are currently transitioning or not.
	 *
	 * @property _verticalSpacing
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_transitioning: false,

	/**
	 * Direction of the current transition.
	 *
	 * @property _transitionDirection
	 * @type String
	 * @default next
	 * @protected
	 */
	_transitionDirection: 'next',

	/**
	 * Provides functionality for gesture based transitions
 	 * between the active and next pages.
	 *
	 * @property _gestures
	 * @type FL.SlideshowGestures
	 * @default null
	 * @protected
	 */
	_gestures: null,

	/**
	 * Initialize image vars.
	 *
	 * @method initializer
	 * @protected
	 */
	initializer: function()
	{
		this._activeImages = [];
		this._nextImages = [];
		this._prevImages = [];
	},

	/**
	 * Renders the UI boxes.
	 *
	 * @method renderUI
	 * @protected
	 */
	renderUI: function()
	{
		this._renderBoxes();
		this._renderNavs();
	},

	/**
	 * Binds the UI events.
	 *
	 * @method bindUI
	 * @protected
	 */
	bindUI: function()
	{
		var root 		= this.get('root'),
			id 			= this.get('id'),
			transition 	= this.get('transition');

		root.on(id + '|albumLoadComplete', this._albumLoadComplete, this);

		if('ontouchstart' in window && this.get('touchSupport')) {

			this._gestures = new Y.FL.SlideshowGestures({
				direction: transition == 'slideVertical' ? 'vertical' : 'horizontal',
				activeItem: this._activePageBox,
				nextItem: this._nextPageBox
			});

			this._gestures.on('moveStart', this._gesturesMoveStart, this);
			this._gestures.on('endComplete', this._gesturesEndComplete, this);
		}
	},

	/**
	 * Syncs the UI boxes.
	 *
	 * @method syncUI
	 * @protected
	 */
	syncUI: function()
	{
		this._syncBoxes();
		this._syncNavs();
	},

	/**
	 * @method destructor
	 * @protected
	 */
	destructor: function()
	{
		var root 	= this.get('root'),
			id 		= this.get('id');

		root.detach(id + '|*');

		Y.FL.SlideshowImageLoader.removeGroup('thumbs');
	},

	/**
	 * Unload all images.
	 *
	 * @method unload
	 */
	unload: function()
	{
		var root 	= this.get('root'),
			id 		= this.get('id'),
			i 		= 0;

		root.detach(id + '|imageLoadComplete');

		Y.FL.SlideshowImageLoader.removeGroup('thumbs');

		for( ; i < this._activeImages.length; i++) {
			this._activeImages[i].unload();
		}
	},

	/**
	 * Resizes the UI boxes.
	 *
	 * @method resize
	 */
	resize: function()
	{
		this._setSizeInfo();
		this._togglePageButtons();
		this._resizeBoxes();
		this._resizeNavs();

		if(this.get('root').albumInfo) {
			Y.FL.SlideshowImageLoader.removeGroup('thumbs');
			this._renderActivePage();
			this._preloadNextPage();
			this._preloadPrevPage();
		}

		// Enable or disable gestures.
		if(this._gestures && this._numPages < 2) {
			this._gestures.disable();
		}
		else if(this._gestures) {
			this._gestures.enable();
		}
	},

	/**
	 * Transitions to the previous page.
	 *
	 * @method prevPage
	 * @protected
	 */
	prevPage: function()
	{
		if(this._transitioning) {
			return;
		}

		this._transitionStart('prev');
	},

	/**
	 * Transitions to the next page.
	 *
	 * @method nextPage
	 * @protected
	 */
	nextPage: function()
	{
		if(this._transitioning) {
			return;
		}

		this._transitionStart('next');
	},

	/**
	 * Called when an album is loaded into the root slideshow widget.
	 *
	 * @method _albumLoadComplete
	 * @protected
	 */
	_albumLoadComplete: function()
	{
		var root 	= this.get('root'),
			id 		= this.get('id');

		root.once(id + '|imageLoadComplete', this.resize, this);
		root.on(id + '|imageLoadComplete', this._imageLoadComplete, this);
	},

	/**
	 * Called when an image is loaded into the root slideshow widget.
	 *
	 * @method _imageLoadComplete
	 * @protected
	 */
	_imageLoadComplete: function()
	{
		var albumInfo 	= this.get('root').albumInfo,
			lastActive 	= Y.one('.fl-slideshow-image-active'),
			lastInfo	= lastActive ? lastActive._imageInfo : null,
			nextActive 	= null,
			nextInfo	= this.get('root').imageInfo;

		this._setActiveImage(this._activeImages);

		nextActive = Y.one('.fl-slideshow-image-active');

		if(lastActive && !nextActive) {
			if(nextInfo.index === 0 && lastInfo.index === albumInfo.images.length - 1) {
				this.nextPage();
			}
			else if(lastInfo.index === 0 && nextInfo.index === albumInfo.images.length - 1) {
				this.prevPage();
			}
			else if(lastInfo.index < nextInfo.index) {
				this.nextPage();
			}
			else if(lastInfo.index > nextInfo.index) {
				this.prevPage();
			}
		}
	},

	/**
	 * Renders the boxes.
	 *
	 * @method _renderBoxes
	 * @protected
	 */
	_renderBoxes: function()
	{
		// Clip box
		this._clipBox = Y.Node.create('<div></div>');
		this._clipBox.addClass('fl-slideshow-thumbs-clip');
		this.get('contentBox').insert(this._clipBox);

		// Pages box
		this._pagesBox = Y.Node.create('<div></div>');
		this._pagesBox.addClass('fl-slideshow-thumbs-pages');
		this._clipBox.insert(this._pagesBox);

		// Active page box
		this._activePageBox = Y.Node.create('<div></div>');
		this._activePageBox.addClass('fl-slideshow-thumbs-page');
		this._pagesBox.insert(this._activePageBox);

		// Next page box
		this._nextPageBox = Y.Node.create('<div></div>');
		this._nextPageBox.addClass('fl-slideshow-thumbs-page');
		this._pagesBox.insert(this._nextPageBox);
	},

	/**
	 * Syncs the boxes.
	 *
	 * @method _syncBoxes
	 * @protected
	 */
	_syncBoxes: function()
	{
		// Active page box
		this._activePageBox.setStyle('left', '0');

		// Next page box
		this._nextPageBox.setStyle('left', '-9999px');
	},

	/**
	 * Resizes the boxes.
	 *
	 * @method _resizeBoxes
	 * @protected
	 */
	_resizeBoxes: function()
	{
		this.set('width', this._bbWidth);
		this.set('height', this._bbHeight);

		this.get('contentBox').setStyle('width', this._cbWidth + 'px');

		this._clipBox.setStyle('width', this._pageWidth + 'px');
		this._clipBox.setStyle('height', this._pageHeight + 'px');
		this._clipBox.setStyle('padding', this._verticalSpacing + 'px 0 0 ' + this._horizontalSpacing + 'px ');
		this._clipBox.setStyle('margin',  '0 0 0 ' + this._clipBoxMarginLeft + 'px');
		this._clipBox.setStyle('top', this._clipBoxTop);

		this._pagesBox.setStyle('width', this._pageWidth + 'px');
		this._pagesBox.setStyle('height', this._pageHeight + 'px');

		this._activePageBox.setStyle('width', this._pageWidth + 'px');
		this._activePageBox.setStyle('height', this._pageHeight + 'px');

		this._nextPageBox.setStyle('width', this._pageWidth + 'px');
		this._nextPageBox.setStyle('height', this._pageHeight + 'px');
	},

	/**
	 * Renders the active page of images.
	 *
	 * @method _renderActivePage
	 * @protected
	 */
	_renderActivePage: function()
	{
		var i 			= 0,
			root		= this.get('root'),
			imageIndex 	= this._imagesPerPage * this._activePageIndex,
			endIndex	= imageIndex + this._imagesPerPage,
			images 		= root.albumInfo.images;

		this._clearActiveImage();

		// Remove current images
		for( ; i < this._activeImages.length; i++) {
			this._activeImages[i].remove();
			this._activeImages[i].unload();
			this._activeImages[i].get('boundingBox')._imageInfo = null;
			this._activeImages[i].get('boundingBox').remove();
		}

		// Draw images
		for(i = 0; imageIndex < endIndex; imageIndex++) {

			if(!images[imageIndex]) {
				break;
			}

			this._renderImage(this._activeImages, i, this._activePageBox, images[imageIndex]);
			i++;
		}

		this._setActiveImage(this._activeImages);
	},

	/**
	 * Renders the next page of images.
	 *
	 * @method _renderNextPage
	 * @protected
	 */
	_renderNextPage: function()
	{
		var i 			= 0,
			imageArray 	= this._transitionDirection == 'next' ? this._nextImages : this._prevImages;

		this._nextPageBox.get('children').remove();

		for( ; i < imageArray.length; i++) {
			if(imageArray[i]._imageInfo) {
				this._renderImage(imageArray, i, this._nextPageBox, imageArray[i]._imageInfo);
			}
			else {
				break;
			}
		}

		this._setActiveImage(imageArray);
	},

	/**
	 * Preloads the next page of images.
	 *
	 * @method _preloadNextPage
	 * @protected
	 */
	_preloadNextPage: function()
	{
		var pageIndex = this._activePageIndex + 1 >= this._numPages ? 0 : this._activePageIndex + 1;

		this._preloadPage(pageIndex, this._nextImages);
	},

	/**
	 * Preloads the previous page of images.
	 *
	 * @method _preloadPrevPage
	 * @protected
	 */
	_preloadPrevPage: function()
	{
		var pageIndex = this._activePageIndex - 1 < 0 ? this._numPages - 1 : this._activePageIndex - 1;

		this._preloadPage(pageIndex, this._prevImages);
	},

	/**
	 * Preloads a page of images.
	 *
	 * @method _preloadPage
	 * @param imageIndex {Number} The image index to start preloading from.
	 * @param imageArray {Array} The array to store the preloaded images.
	 * @protected
	 */
	_preloadPage: function(pageIndex, imageArray)
	{
		var i 			= 0,
			root		= this.get('root'),
			images 		= root.albumInfo.images,
			imageIndex	= pageIndex * this._imagesPerPage,
			endIndex	= imageIndex + this._imagesPerPage,
			imageConfig = this.get('imageConfig'),
			width 		= imageConfig.width,
			height 		= imageConfig.height;

		if(this._numPages > 1) {

			// Unload existing images
			for( ; i < imageArray.length; i++) {
				imageArray[i].remove();
				imageArray[i].unload();
			}

			// Preload the images
			for(i = 0; imageIndex < endIndex; imageIndex++) {

				if(!images[imageIndex]) {
					continue;
				}

				this._renderImage(imageArray, i);
				imageArray[i].preload(images[imageIndex], width, height);
				i++;
			}
		}
	},

	/**
	 * Renders an image.
	 *
	 * @method _renderImage
	 * @protected
	 */
	_renderImage: function(imageArray, i, page, imageInfo)
	{
		var imageBB		= null,
			imageConfig = this.get('imageConfig');

		// Create the image?
		if(typeof imageArray[i] == 'undefined') {
			imageConfig.loadGroup = 'thumbs';
			imageConfig.useThumbSizes = true;
			imageConfig.loadVideos = false;
			imageArray[i] = new Y.FL.SlideshowImage(imageConfig);
			imageBB = imageArray[i].get('boundingBox');
			imageBB.on('click', this._imageClick, this);
			imageBB.on('mouseover', this._imageMouseover, this);
			imageBB.on('mouseout', this._imageMouseout, this);
		}

		// Image bounding box
		imageBB = imageArray[i].get('boundingBox');
		imageBB.setStyle('margin', '0 ' + this._horizontalSpacing + 'px ' + this._verticalSpacing + 'px 0');

		// Add the image to a page?
		if(page) {
			this._childrenContainer = page;
			this.add(imageArray[i]);
			imageArray[i].resize(imageConfig.width, imageConfig.height);
		}

		// Load the image?
		if(imageInfo) {
			imageArray[i].load(imageInfo);
			imageBB._imageInfo = imageInfo;
		}
	},

	/**
     * Overrides the WidgetParent _uiAddChild method so _renderImage
     * will render to the appropriate page.
     *
     * @method _uiAddChild
     * @protected
     * @param child {Widget} The child Widget instance to render.
     * @param parentNode {Object} The Node under which the
     * child Widget is to be rendered. Set to the appropriate page
     * in the _renderImage method by setting _childrenContainer.
     */
    _uiAddChild: function (child, parentNode)
    {
        child.render(parentNode);
        parentNode.appendChild(child.get('boundingBox'));
    },

	/**
	 * Called when an image is clicked.
	 *
	 * @method _imageClick
	 * @protected
	 */
	_imageClick: function(e)
	{
		var root = this.get('root');

		if(this.get('pauseOnClick')) {
			root.pause();
		}

		root.loadImage(e.currentTarget._imageInfo.index);

		/**
		 * Fires when an image is clicked.
		 *
		 * @event imageClick
		 */
		this.fire('imageClick');
	},

	/**
	 * Sets the active image.
	 *
	 * @method _setActiveImage
	 * @param imageArray {Array} The image array to check for the active image.
	 * @protected
	 */
	_setActiveImage: function(imageArray)
	{
		var i = 0;

		this._clearActiveImage();

		for( ; i < imageArray.length; i++) {
			if(imageArray[i]._imageInfo) {
				if(imageArray[i]._imageInfo.index == this.get('root').imageInfo.index) {
					imageArray[i].get('boundingBox').addClass('fl-slideshow-image-active');
					break;
				}
			}
		}
	},

	/**
	 * Removes the class name 'fl-slideshow-image-active'
	 * from the active image.
	 *
	 * @method _clearActiveImage
	 * @protected
	 */
	_clearActiveImage: function()
	{
		var active = Y.one('.fl-slideshow-image-active');

		if(active) {
			active.removeClass('fl-slideshow-image-active');
		}
	},

	/**
	 * Gets the transition type.
	 *
	 * @method _getTransition
	 * @protected
	 */
	_getTransition: function()
	{
		var transition = this.get('transition');

		if(transition == 'slideHorizontal' && this._transitionDirection == 'next') {
			return 'slideLeft';
		}
		else if(transition == 'slideHorizontal' && this._transitionDirection == 'prev') {
			return 'slideRight';
		}
		else if(transition == 'slideVertical' && this._transitionDirection == 'next') {
			return 'slideUp';
		}
		else if(transition == 'slideVertical' && this._transitionDirection == 'prev') {
			return 'slideDown';
		}

		return transition;
	},

	/**
	 * Starts the transition, moving in the provided direction.
	 *
	 * @method _transitionStart
	 * @param direction {String} The direction to transition.
	 * @protected
	 */
	_transitionStart: function(direction)
	{
		if(this._numPages > 1) {

			Y.FL.SlideshowImageLoader.removeGroup('thumbs');

			this._transitionDirection = direction;
			this._transitioning = true;
			this._nextPageBox.setStyle('left', '0px');
			this._renderNextPage();

			this._transition = new Y.FL.SlideshowTransition({
				itemIn: this._nextPageBox,
				itemOut: this._activePageBox,
				type: this._getTransition(),
				duration: this.get('transitionDuration'),
				easing: this.get('transitionEasing')
			});

			this._transition.once('complete', this._transitionComplete, this);
			this._transition.run();

			// Disable gestures if set.
			if(this._gestures) {
				this._gestures.disable();
			}
		}
	},

	/**
	 * Transition cleanup called when the current transition ends.
	 *
	 * @method _transitionComplete
	 * @protected
	 */
	_transitionComplete: function()
	{
		this._swapPageRefs();
		this._transitioning = false;
		this._transitionDirection = '';
		this._transition = null;

		// Enable gestures if set.
		if(this._gestures) {
			this._gestures.enable();
		}

		/**
		 * Fires when a page transition completes.
		 *
		 * @event transitionComplete
		 */
		this.fire('transitionComplete');
	},

	/**
	 * @method _gesturesMoveStart
	 * @param e {Object} The event object.
	 * @protected
	 */
	_gesturesMoveStart: function(e)
	{
		Y.FL.SlideshowImageLoader.removeGroup('thumbs');

		this._transitionDirection = e.direction;
		this._renderNextPage();
	},

	/**
	 * @method _gesturesEndComplete
	 * @protected
	 */
	_gesturesEndComplete: function()
	{
		this._swapPageRefs();
		this._transitionDirection = '';
		this.fire('transitionComplete');
	},

	/**
	 * Swaps the active page and next page references when
	 * a transition completes and sets the active page index.
	 *
	 * @method _swapPageRefs
	 * @protected
	 */
	_swapPageRefs: function()
	{
		var lastBox 	= this._activePageBox,
			lastImages 	= this._activeImages;

		this._activePageBox = this._nextPageBox;
		this._nextPageBox = lastBox;
		this._nextPageBox.setStyle('left', '-9999px');

		if(this._transitionDirection == 'next') {
			this._activeImages = this._nextImages;
			this._nextImages = lastImages;
		}
		else {
			this._activeImages = this._prevImages;
			this._prevImages = lastImages;
		}

		// Active page index
		if(this._transitionDirection == 'next' && this._activePageIndex + 1 < this._numPages) {
			this._activePageIndex++;
		}
		else if(this._transitionDirection == 'next') {
			this._activePageIndex = 0;
		}
		else if(this._transitionDirection == 'prev' && this._activePageIndex - 1 > -1) {
			this._activePageIndex--;
		}
		else if(this._transitionDirection == 'prev') {
			this._activePageIndex = this._numPages - 1;
		}

		// Swap gesture refs
		if(this._gestures) {
			this._gestures.set('activeItem', this._activePageBox);
			this._gestures.set('nextItem', this._nextPageBox);
		}

		this._preloadNextPage();
		this._preloadPrevPage();
	},

	/**
	 * Renders the enabled navs.
	 *
	 * @method _renderNavs
	 * @protected
	 */
	_renderNavs: function()
	{
		var topNavButtons 		= this.get('topNavButtons'),
			rightNavButtons 	= this.get('rightNavButtons'),
			bottomNavButtons 	= this.get('bottomNavButtons'),
			leftNavButtons 		= this.get('leftNavButtons');

		if(this.get('topNavEnabled') && topNavButtons.length > 0) {
			this._topNav = new Y.FL.SlideshowNav({ buttons: topNavButtons });
			this._topNav.get('boundingBox').addClass('fl-slideshow-thumbs-top-nav');
			this.add(this._topNav);
			this._topNav.render(this.get('contentBox'));
			this._clipBox.insert(this._topNav.get('boundingBox'), 'before');
			this._bindNavEvents(this._topNav);
		}
		if(this.get('rightNavEnabled') && rightNavButtons.length > 0) {
			this._rightNav = new Y.FL.SlideshowNav({ buttons: rightNavButtons });
			this._rightNav.get('boundingBox').addClass('fl-slideshow-thumbs-right-nav');
			this.add(this._rightNav);
			this._rightNav.render(this.get('contentBox'));
			this._bindNavEvents(this._rightNav);
		}
		if(this.get('bottomNavEnabled') && bottomNavButtons.length > 0) {
			this._bottomNav = new Y.FL.SlideshowNav({ buttons: bottomNavButtons });
			this._bottomNav.get('boundingBox').addClass('fl-slideshow-thumbs-bottom-nav');
			this.add(this._bottomNav);
			this._bottomNav.render(this.get('contentBox'));
			this._bindNavEvents(this._bottomNav);
		}
		if(this.get('leftNavEnabled') && leftNavButtons.length > 0) {
			this._leftNav = new Y.FL.SlideshowNav({ buttons: leftNavButtons });
			this._leftNav.get('boundingBox').addClass('fl-slideshow-thumbs-left-nav');
			this.add(this._leftNav);
			this._leftNav.render(this.get('contentBox'));
			this._bindNavEvents(this._leftNav);
		}
	},

	/**
	 * Syncs the navs.
	 *
	 * @method _syncNavs
	 * @protected
	 */
	_syncNavs: function()
	{
		var rightNavBB, bottomNavBB, leftNavBB;

		if(this._rightNav) {
			rightNavBB = this._rightNav.get('boundingBox');
			rightNavBB.setStyle('position', 'absolute');
			rightNavBB.setStyle('top', '0px');
			rightNavBB.setStyle('right', '0px');
		}
		if(this._bottomNav) {
			bottomNavBB = this._bottomNav.get('boundingBox');
			bottomNavBB.setStyle('position', 'absolute');
			bottomNavBB.setStyle('bottom', '0px');
			bottomNavBB.setStyle('width', '100%');
		}
		if(this._leftNav) {
			leftNavBB = this._leftNav.get('boundingBox');
			leftNavBB.setStyle('position', 'absolute');
			leftNavBB.setStyle('top', '0px');
			leftNavBB.setStyle('left', '0px');
		}
	},

	/**
	 * Resizes the navs.
	 *
	 * @method _resizeNavs
	 * @protected
	 */
	_resizeNavs: function()
	{
		var rightNavBB,
			leftNavBB,
			marginTop;

		if(this._rightNav) {
			rightNavBB = this._rightNav.get('boundingBox');
			marginTop = this._bbHeight/2 - parseInt(rightNavBB.getComputedStyle('height'), 10)/2;
			rightNavBB.setStyle('marginTop', marginTop + 'px');
		}
		if(this._leftNav) {
			leftNavBB = this._leftNav.get('boundingBox');
			marginTop = this._bbHeight/2 - parseInt(leftNavBB.getComputedStyle('height'), 10)/2;
			leftNavBB.setStyle('marginTop', marginTop + 'px');
		}
	},

	/**
	 * Binds events to the provided nav.
	 *
	 * @method _bindNavEvents
	 * @param nav {Object} The nav to bind to.
	 * @protected
	 */
	_bindNavEvents: function(nav)
	{
		if(nav._buttons.prevPage) {
			nav._buttons.prevPage.on('click', this.prevPage, this);
		}
		if(nav._buttons.nextPage) {
			nav._buttons.nextPage.on('click', this.nextPage, this);
		}

		nav.on('resize', this.resize, this);
	},

	/**
	 * Hides the prev page and next page buttons
	 * if there is only one page of thumbs.
	 *
	 * @method _togglePageButtons
	 * @protected
	 */
	_togglePageButtons: function()
	{
		var buttons = this.get('boundingBox').all('.fl-slideshow-nav-prevPage, .fl-slideshow-nav-nextPage'),
			display = buttons.getStyle('display')[0];

		if(this._numPages == 1 && display == 'inline-block') {
			buttons.setStyle('display', 'none');
			this._setSizeInfo();
		}
		else if(this._numPages > 1 && display == 'none') {
			buttons.setStyle('display', 'inline-block');
			this._setSizeInfo();
		}
	},

	/**
	 * Sets the size info used when resizing and loading pages.
	 *
	 * @method _setSizeInfo
	 * @protected
	 */
	_setSizeInfo: function()
	{
		var root					= this.get('root'),
			bb						= this.get('boundingBox'),
			bbPosition              = bb.getStyle('position'),
			bbLeftMargin			= parseInt(bb.getStyle('marginLeft'), 10),
			bbRightMargin			= parseInt(bb.getStyle('marginRight'), 10),
			bbTopMargin				= parseInt(bb.getStyle('marginTop'), 10),
			bbBottomMargin			= parseInt(bb.getStyle('marginBottom'), 10),
			bbLeftPadding			= parseInt(bb.getStyle('paddingLeft'), 10),
			bbRightPadding			= parseInt(bb.getStyle('paddingRight'), 10),
			bbTopPadding			= parseInt(bb.getStyle('paddingTop'), 10),
			bbBottomPadding			= parseInt(bb.getStyle('paddingBottom'), 10),
			parent 					= bb.get('parentNode'),
			parentWidth 			= parseInt(parent.getComputedStyle('width'), 10),
			parentHeight 			= parseInt(parent.getComputedStyle('height'), 10),
			bbWidth 				= parentWidth - bbLeftPadding - bbRightPadding - bbLeftMargin - bbRightMargin,
			bbHeight 				= parentHeight - bbTopPadding - bbBottomPadding - bbTopMargin - bbBottomMargin,
			cbWidth					= bbWidth,
			pageWidth 				= bbWidth,
			pageHeight 				= bbHeight,
			columns 				= this.get('columns'),
			rows 					= this.get('rows'),
			imageConfig				= this.get('imageConfig'),
			horizontalSpacing		= this.get('horizontalSpacing'),
			verticalSpacing			= this.get('verticalSpacing'),
			spaceEvenly				= this.get('spaceEvenly'),
			centerSinglePage		= this.get('centerSinglePage'),
			leftNavWidth			= 0,
			rightNavWidth			= 0,
			topNavHeight			= 0,
			bottomNavHeight			= 0,
			colsPerPage 			= columns,
			rowsPerPage				= rows,
			imagesPerPage			= 0,
			numPages				= 1,
			clipBoxMarginLeft		= 0,
			clipBoxTop				= 0,
			availHorizSpace			= 0,
			availVerticalSpace		= 0;

        // Position absolute causes some resizing bugs.
        bb.setStyle('position', 'relative');

		// Bounding box width
		if(!isNaN(columns)) {
			bbWidth = pageWidth = columns * (imageConfig.width + horizontalSpacing) + horizontalSpacing;
		}

		// Bounding box height
		if(!isNaN(rows)) {
			bbHeight = pageHeight = rows * (imageConfig.height + verticalSpacing) + verticalSpacing;
		}

		// Compensate for the navs
		if(this._leftNav) {

			leftNavWidth = parseInt(this._leftNav.get('boundingBox').getComputedStyle('width'), 10);

			if(isNaN(columns)) {
				pageWidth -= leftNavWidth;
			}
			else {
				bbWidth += leftNavWidth;
			}
		}
		if(this._rightNav) {

			rightNavWidth = parseInt(this._rightNav.get('boundingBox').getComputedStyle('width'), 10);

			if(isNaN(columns)) {
				pageWidth -= rightNavWidth;
			}
			else {
				bbWidth += rightNavWidth;
			}
		}
		if(this._topNav) {

			topNavHeight = parseInt(this._topNav.get('boundingBox').getComputedStyle('height'), 10);

			if(isNaN(rows)) {
				pageHeight -= topNavHeight;
			}
			else {
				bbHeight += topNavHeight;
			}
		}
		if(this._bottomNav) {

			bottomNavHeight = parseInt(this._bottomNav.get('boundingBox').getComputedStyle('height'), 10);

			if(isNaN(rows)) {
				pageHeight -= bottomNavHeight;
			}
			else {
				bbHeight += bottomNavHeight;
			}
		}

		// Columns per page
		if(isNaN(columns)) {
			colsPerPage = Math.floor(pageWidth/(imageConfig.width + horizontalSpacing));
			colsPerPage = colsPerPage < 1 ? 1 : colsPerPage;
		}

		// Rows per page
		if(isNaN(rows)) {
			rowsPerPage = Math.floor(pageHeight/(imageConfig.height + verticalSpacing));
			rowsPerPage = rowsPerPage < 1 ? 1 : rowsPerPage;
		}

		// Images per page
		imagesPerPage = colsPerPage * rowsPerPage;

		// Number of pages
		if(root.albumInfo) {
			numPages = Math.ceil(root.albumInfo.images.length/imagesPerPage);
		}

		// Horizontal spacing
		if(isNaN(columns) && spaceEvenly) {
			horizontalSpacing = Math.floor((pageWidth - (imageConfig.width * colsPerPage))/(colsPerPage + 1));
		}

		// Vertical spacing
		if(isNaN(rows) && spaceEvenly) {
			verticalSpacing = Math.floor((pageHeight - (imageConfig.height * rowsPerPage))/(rowsPerPage + 1));
		}

		// Content container width
		if(root.albumInfo && centerSinglePage && numPages == 1 && rowsPerPage == 1) {

			cbWidth = root.albumInfo.images.length * imageConfig.width;
			cbWidth += horizontalSpacing * (root.albumInfo.images.length + 1);

			if(this._leftNav) {
				cbWidth += leftNavWidth;
			}
			if(this._rightNav) {
				cbWidth += rightNavWidth;
			}
		}
		else {
			cbWidth = bbWidth;
		}

		// Final page width and height
		if(root.albumInfo && centerSinglePage && numPages == 1 && rowsPerPage == 1) {
			pageWidth = root.albumInfo.images.length * imageConfig.width;
			pageWidth += horizontalSpacing * root.albumInfo.images.length;
		}
		else {
			pageWidth = colsPerPage * (imageConfig.width + horizontalSpacing);
		}

		pageHeight = rowsPerPage * (imageConfig.height + verticalSpacing);

		// Clip box margin left
		if(numPages < 2) {
			clipBoxMarginLeft = leftNavWidth;
		}
		else {
			availHorizSpace = bbWidth;

			if(this._rightNav) {
				availHorizSpace -= rightNavWidth;
			}
			if(this._leftNav) {
				availHorizSpace -= leftNavWidth;
				clipBoxMarginLeft = leftNavWidth + (availHorizSpace - pageWidth - horizontalSpacing)/2;
			}
			else {
				clipBoxMarginLeft = (availHorizSpace - pageWidth - horizontalSpacing)/2;
			}
		}

		// Clip box margin top
		if(numPages > 1 && !spaceEvenly) {

			availVerticalSpace = bbHeight;

			if(this._topNav) {
				availVerticalSpace -= topNavHeight;
			}
			if(this._bottomNav) {
				availVerticalSpace -= bottomNavHeight;
			}

			clipBoxTop = (availVerticalSpace - (verticalSpacing + pageHeight))/2;
		}

		// Set the info
		this._bbHeight = bbHeight;
		this._bbWidth = bbWidth;
		this._cbWidth = cbWidth;
		this._clipBoxMarginLeft = clipBoxMarginLeft;
		this._clipBoxTop = clipBoxTop;
		this._colsPerPage = colsPerPage;
		this._rowsPerPage = rowsPerPage;
		this._imagesPerPage = imagesPerPage;
		this._numPages = numPages;
		this._pageHeight = pageHeight;
		this._pageWidth = pageWidth;
		this._leftNavWidth = leftNavWidth;
		this._rightNavWidth = rightNavWidth;
		this._horizontalSpacing = horizontalSpacing;
		this._verticalSpacing = verticalSpacing;
		this._activePageIndex = Math.floor(root.imageIndex/this._imagesPerPage);

		// Set back to the initial position.
        bb.setStyle('position', bbPosition);
	}

}, {

	/**
	 * Custom CSS class name for the widget.
	 *
	 * @property CSS_PREFIX
	 * @type String
	 * @protected
	 * @static
	 */
	CSS_PREFIX: 'fl-slideshow-thumbs',

	/**
	* Static property used to define the default attribute configuration of
	* the Widget.
	*
	* @property ATTRS
	* @type Object
	* @protected
	* @static
	*/
	ATTRS: {

		/**
		 * The number of thumbnail columns. If set to auto, the number of
		 * columns will be calculated based on the width of the parent node.
		 *
		 * @attribute columns
		 * @type String or Number
		 * @default auto
		 */
		columns: {
			value: 'auto'
		},

		/**
		 * The number of thumbnail rows. If set to auto, the number of
		 * rows will be calculated based on the height of the parent node.
		 *
		 * @attribute rows
		 * @type String or Number
		 * @default auto
		 */
		rows: {
			value: 'auto'
		},

		/**
		 * The horizontal spacing between thumbs.
		 *
		 * @attribute horizontalSpacing
		 * @type Number
		 * @default 15
		 */
		horizontalSpacing: {
			value: 15
		},

		/**
		 * The vertical spacing between thumbs.
		 *
		 * @attribute verticalSpacing
		 * @type Number
		 * @default 15
		 */
		verticalSpacing: {
			value: 15
		},

		/**
		 * Whether to space the thumbs evenly within a page.
		 *
		 * @attribute spaceEvenly
		 * @type Boolean
		 * @default true
		 */
		spaceEvenly: {
			value: true
		},

		/**
		 * Whether to center single pages of thumbs.
		 *
		 * @attribute centerSinglePage
		 * @type Boolean
		 * @default false
		 */
		centerSinglePage: {
			value: true
		},

		/**
		 * Whether to pause the parent slideshow when a thumb is clicked.
		 *
		 * @attribute pauseOnClick
		 * @type Boolean
		 * @default false
		 */
		pauseOnClick: {
			value: false
		},

		/**
		 * The type of transition to use between pages.
		 *
		 * @attribute transition
		 * @type String
		 * @default slideHorizontal
		 */
		transition: {
			value: 'slideHorizontal'
		},

		/**
		 * The duration of the transition between pages.
		 *
		 * @attribute transitionDuration
		 * @type Number
		 * @default 0.8
		 */
		transitionDuration: {
			value: 0.8
		},

		/**
		 * The type of transition easing to use between pages.
		 *
		 * @attribute transitionEasing
		 * @type String
		 * @default ease-out
		 */
		transitionEasing: {
			value: 'ease-out'
		},

		/**
		 * The configuration object used to create new instances of
		 * FL.SlideshowImage. See the API docs for {@link FL.SlideshowImage}
		 * for a complete list of configuration attributes.
		 *
		 * @attribute imageConfig
		 * @type Object
		 * @default {}
		 */
		imageConfig: {
			value: {
				crop: true,
				width: 50,
				height: 50
			}
		},

		/**
		 * Whether to use the top nav or not.
		 *
		 * @attribute topNavEnabled
		 * @type Boolean
		 * @default false
		 */
		topNavEnabled: {
			value: false
		},

		/**
		 * An array of button names used to render the top nav buttons.
		 *
		 * @attribute topNavButtons
		 * @type Array
		 * @default prevPage, nextPage
		 */
		topNavButtons: {
			value: ['prevPage', 'nextPage']
		},

		/**
		 * Whether to use the right nav or not.
		 *
		 * @attribute rightNavEnabled
		 * @type Boolean
		 * @default true
		 */
		rightNavEnabled: {
			value: true
		},

		/**
		 * An array of button names used to render the right nav buttons.
		 *
		 * @attribute rightNavButtons
		 * @type Array
		 * @default nextPage
		 */
		rightNavButtons: {
			value: ['nextPage']
		},

		/**
		 * Whether to use the bottom nav or not.
		 *
		 * @attribute bottomNavEnabled
		 * @type Boolean
		 * @default false
		 */
		bottomNavEnabled: {
			value: false
		},

		/**
		 * An array of button names used to render the bottom nav buttons.
		 *
		 * @attribute bottomNavButtons
		 * @type Array
		 * @default prevPage, nextPage
		 */
		bottomNavButtons:{
			value: ['prevPage', 'nextPage']
		},

		/**
		 * Whether to use the left nav or not.
		 *
		 * @attribute leftNavEnabled
		 * @type Boolean
		 * @default true
		 */
		leftNavEnabled: {
			value: true
		},

		/**
		 * An array of button names used to render the left nav buttons.
		 *
		 * @attribute leftNavButtons
		 * @type Array
		 * @default prevPage
		 */
		leftNavButtons:{
			value: ['prevPage']
		},

		/**
		 * Whether to use touch gestures, when available,
		 * to transition between pages or not.
		 *
		 * @attribute touchSupport
		 * @type Boolean
		 * @default false
		 */
		touchSupport: {
			value: false
		}
	}
});

/**
 * Provides functionality for transitions between slideshow components.
 *
 * @namespace FL
 * @class SlideshowTransition
 * @constructor
 * @param config {Object} Configuration object
 * @extends Base
 */
Y.namespace('FL').SlideshowTransition = Y.Base.create('fl-slideshow-transition', Y.Base, [], {

	/**
	 * The transition function to use when run is called.
	 *
	 * @property _transitionFunction
	 * @type String
	 * @default _transitionFade
	 * @protected
	 */
	_transitionFunction: '_transitionFade',

	/**
	 * The current transition type.
	 *
	 * @property _type
	 * @type String
	 * @default fade
	 * @protected
	 */
	_type: 'fade',

	/**
	 * Parses the transition type and sets the _transitionFunction
	 * used when run is called.
	 *
	 * @method initializer
	 * @protected
	 */
	initializer: function()
	{
		var type 		               = this.get('type'),
			typeArray	               = [],
			types                      = Y.FL.SlideshowTransition.TYPES,
			slideshowImageTypes        = Y.FL.SlideshowTransition.SLIDESHOW_IMAGE_TYPES,
			isSlideshowImageTransition = Y.Array.indexOf(slideshowImageTypes, type) > -1,
			isSlideshowImage           = this._isSlideshowImage(),
			itemIn 				       = this.get('itemIn'),
			itemOut 			       = this.get('itemOut');

        // Check for random transitions.
		if(type.indexOf(',') > -1) {
			typeArray = type.split(',');
			typeArray.sort(function() { return 0.5 - Math.random(); });
			type = typeArray[0];
		}

		// Make sure we can run this transition, otherwise set a fallback.
		if(!isSlideshowImage && isSlideshowImageTransition) {
            type = 'fade';
		}
		else if(isSlideshowImage) {
            if((itemIn && itemIn.one('img') === null) || (itemOut && itemOut.one('img') === null)) {
                type = 'none';
            }
            else if(isSlideshowImageTransition) {
                if((Y.UA.gecko && Y.UA.gecko < 5) || Y.UA.opera > 0 || (Y.UA.ie > 0 && Y.UA.ie < 9)) {
                    type = 'fade';
                }
            }
        }

		// Set the transition function and type.
		if(Y.FL.SlideshowTransition.TYPES[type]) {
			this._transitionFunction = types[type];
			this._type = type;
		}

		// Setup the items.
		this._setupItems();
	},

	/**
	 * Fires the start event and calls the transition function.
	 *
	 * @method run
	 */
	run: function()
	{
		/**
		 * Fires when the transition starts.
		 *
		 * @event start
		 */
		this.fire('start');

		this[this._transitionFunction].call(this);
	},

	/**
	 * Set initial styles for the items.
	 *
	 * @method _setupItems
	 * @protected
	 */
	_setupItems: function()
	{
        var itemIn  = this.get('itemIn'),
			itemOut = this.get('itemOut');

		if(itemIn) {
			itemIn.setStyle('zIndex', 2);
			itemIn.setStyle('opacity', 1);

			if(Y.FL.Utils.cssSupport('transform')) {
    			itemIn.setStyle('transform', 'translate(0, 0)');
    		}
    		else {
    			itemIn.setStyle('top', '0');
    			itemIn.setStyle('left', '0');
    		}
		}
		if(itemOut) {
			itemOut.setStyle('zIndex', 1);
		}
	},

	/**
	 * Checks if the transition is being run
	 * on an instance of FL.SlideshowImage or not.
	 *
	 * @method _isSlideshowImage
	 * @protected
	 */
	_isSlideshowImage: function()
	{
        var itemIn 	= this.get('itemIn'),
			itemOut = this.get('itemOut');

        if(itemIn && itemIn.hasClass('fl-slideshow-image')) {
			return true;
		}
		else if(itemOut && itemOut.hasClass('fl-slideshow-image')) {
			return true;
		}

		return false;
	},

	/**
	 * Starts the transtion using the provided property objects.
	 *
	 * @method _transitionStart
	 * @param propsIn {Object} The properties to animate in.
	 * @param propsOut {Object} The properties to animate out.
	 * @protected
	 */
	_transitionStart: function(propsIn, propsOut)
	{
		var itemIn 				= this.get('itemIn'),
			itemOut 			= this.get('itemOut'),
			itemInCallback		= Y.bind(this._transitionComplete, this),
			itemOutCallback 	= !itemIn ? itemInCallback : null,
			duration 			= this.get('duration'),
			easing 				= this.get('easing');

		if(itemIn) {
			propsIn.duration = propsIn.duration || duration;
			propsIn.easing = propsIn.easing || easing;
			itemIn.transition(propsIn);
		}
		if(itemOut) {
			propsOut.duration = propsOut.duration || duration;
			propsOut.easing = propsOut.easing || easing;
			itemOut.transition(propsOut);
		}

		if(itemInCallback) {
			Y.later(propsIn.duration * 1000 + 100, null, itemInCallback);
		}
		else if(itemOutCallback) {
			Y.later(propsOut.duration * 1000 + 100, null, itemOutCallback);
		}
	},

	/**
	 * Clean up method called when the transition completes.
	 *
	 * @method _transitionComplete
	 * @protected
	 */
	_transitionComplete: function()
	{
		this._set('itemIn', null);
		this._set('itemOut', null);

		/**
		 * Fires when the transition completes.
		 *
		 * @event complete
		 */
		this.fire('complete');
	},

	/**
	 * No transition.
	 *
	 * @method _transitionNone
	 * @protected
	 */
	_transitionNone: function()
	{
		var itemIn  = this.get('itemIn'),
			itemOut = this.get('itemOut');

		if(itemIn) {
			itemIn.setStyle('opacity', 1);
		}
		if(itemOut) {
			itemOut.setStyle('opacity', 0);
		}

		this._transitionComplete();
	},

	/**
	 * Fade transition.
	 *
	 * @method _transitionFade
	 * @protected
	 */
	_transitionFade: function()
	{
		var itemIn = this.get('itemIn');

		if(itemIn) {
			itemIn.setStyle('opacity', 0);
		}

		this._transitionStart({ opacity: 1 },{ opacity: 0 });
	},

	/**
	 * Slide left transition.
	 *
	 * @method _transitionSlideLeft
	 * @protected
	 */
	_transitionSlideLeft: function()
	{
		if(Y.FL.Utils.cssSupport('transform')) {
			this._cssTransitionSlide({
				inStart: 'translate(100%, 0)',
				inEnd: 'translate(0, 0)',
				outStart: 'translate(0, 0)',
				outEnd: 'translate(-100%, 0)'
			});
		}
		else {
			this._jsTransitionSlide('left');
		}
	},

	/**
	 * Slide right transition.
	 *
	 * @method _transitionSlideRight
	 * @protected
	 */
	_transitionSlideRight: function()
	{
		if(Y.FL.Utils.cssSupport('transform')) {
			this._cssTransitionSlide({
				inStart: 'translate(-100%, 0)',
				inEnd: 'translate(0, 0)',
				outStart: 'translate(0, 0)',
				outEnd: 'translate(100%, 0)'
			});
		}
		else {
			this._jsTransitionSlide('right');
		}
	},

	/**
	 * Slide up transition.
	 *
	 * @method _transitionSlideUp
	 * @protected
	 */
	_transitionSlideUp: function()
	{
		if(Y.FL.Utils.cssSupport('transform')) {
			this._cssTransitionSlide({
				inStart: 'translate(0, 100%)',
				inEnd: 'translate(0, 0)',
				outStart: 'translate(0, 0)',
				outEnd: 'translate(0, -100%)'
			});
		}
		else {
			this._jsTransitionSlide('up');
		}
	},

	/**
	 * Slide down transition.
	 *
	 * @method _transitionSlideDown
	 * @protected
	 */
	_transitionSlideDown: function()
	{
		if(Y.FL.Utils.cssSupport('transform')) {
			this._cssTransitionSlide({
				inStart: 'translate(0, -100%)',
				inEnd: 'translate(0, 0)',
				outStart: 'translate(0, 0)',
				outEnd: 'translate(0, 100%)'
			});
		}
		else {
			this._jsTransitionSlide('down');
		}
	},

	/**
	 * JavaScript slide transition.
	 *
	 * @method _jsTransitionSlide
	 * @protected
	 */
	_jsTransitionSlide: function(direction)
	{
		var itemIn 		= this.get('itemIn'),
			itemOut 	= this.get('itemOut'),
			itemOutEnd	= 0;

		// Item Out
		if(itemOut && direction == 'left') {
			itemOutEnd = -parseInt(itemOut.getStyle('width'), 10);
		}
		if(itemOut && direction == 'right') {
			itemOutEnd = parseInt(itemOut.getStyle('width'), 10);
		}
		if(itemOut && direction == 'up') {
			itemOutEnd = -parseInt(itemOut.getStyle('height'), 10);
		}
		if(itemOut && direction == 'down') {
			itemOutEnd = parseInt(itemOut.getStyle('height'), 10);
		}

		// Item In
		if(itemIn) {
			itemIn.setStyle('opacity', 1);
		}
		if(itemIn && direction == 'left') {
			itemIn.setStyle('left', itemIn.getStyle('width'));
		}
		if(itemIn && direction == 'right') {
			itemIn.setStyle('left', '-' + itemIn.getStyle('width'));
		}
		if(itemIn && direction == 'up') {
			itemIn.setStyle('top', itemIn.getStyle('height'));
		}
		if(itemIn && direction == 'down') {
			itemIn.setStyle('top', '-' + itemIn.getStyle('height'));
		}

		// Transition Start
		if(direction == 'left' || direction == 'right') {
			this._transitionStart({ left: 0 },{ left: itemOutEnd });
		}
		else {
			this._transitionStart({ top: 0 },{ top: itemOutEnd });
		}
	},

	/**
	 * CSS slide transition.
	 *
	 * @method _cssTransitionSlide
	 * @protected
	 */
	_cssTransitionSlide: function(props)
	{
		var itemIn 	        = this.get('itemIn'),
			itemOut         = this.get('itemOut'),
			transformProp   = Y.UA.chrome < 36 ? 'transform' : '-webkit-transform',
			inProps         = {},
			outProps        = {};

        inProps[transformProp] = props.inEnd;
        outProps[transformProp] = props.outEnd;

		if(itemIn) {
			itemIn.setStyle('transition', '');
			itemIn.setStyle('opacity', 1);
			itemIn.setStyle(transformProp, props.inStart);
		}
		if(itemOut) {
			itemOut.setStyle('transition', '');
			itemOut.setStyle(transformProp, props.outStart);
		}

		this._transitionStart(inProps, outProps);
	},

	/**
	 * Bars and blinds transition.
	 *
	 * @method _transitionBars
	 * @protected
	 */
	_transitionBars: function()
	{
        // Hide the image until the slices have transitioned in.
        this.get('itemIn').one('.fl-slideshow-image-img').setStyle('opacity', 0);

        var numBars   = this.get('bars'),
            slices    = this._renderSlices(1, numBars),
            duration  = this.get('duration'),
            delay     = 0,
            increment = 100,
            last      = false,
            i         = 0,
            clone     = null,
            props     = {
                duration: duration,
                opacity: 1
            };

        // barsRandom
        if(this._type == 'barsRandom') {
            slices = this._randomizeSlices(slices);
        }

        // Transition the slices.
        for( ; i < slices.length; i++) {

            // Make a clone of our transition properties.
            clone = Y.clone(props);

            // blinds
            if(this._type == 'blinds') {
                clone.width = parseFloat(slices[i].getComputedStyle('width'), 10) + 'px';
                slices[i].setStyle('width', '0px');
                increment = 50;
            }

            // Run the transition.
            last = i == slices.length - 1 ? true : false;
            Y.later(delay, this, this._transitionSlice, [slices[i], clone, last]);
            delay += increment;
        }

        this._transitionSlicesFadeLast(delay);
	},

	/**
	 * Boxes transition.
	 *
	 * @method _transitionBoxes
	 * @protected
	 */
	_transitionBoxes: function()
	{
        // Hide the image until the slices have transitioned in.
        this.get('itemIn').one('.fl-slideshow-image-img').setStyle('opacity', 0);

        var numCols     = this.get('boxCols'),
            numRows     = this.get('boxRows'),
            numSlices   = numCols * numRows,
            multi       = this._type != 'boxesRandom',
            slices      = this._renderSlices(numRows, numCols, multi),
            duration    = this.get('duration'),
            delay       = 0,
            increment   = 150,
            last      = false,
            i           = 0,
            row         = 0,
            col         = 0,
            startCol    = -1,
            clone       = null,
            props       = {
                duration: duration,
                opacity: 1
            };

        // boxesRandom
        if(!multi) {

            slices = this._randomizeSlices(slices);
            increment = 30;

            for( ; i < slices.length; i++) {
                clone = Y.clone(props);
                last = i == slices.length - 1 ? true : false;
                Y.later(delay, this, this._transitionSlice, [slices[i], clone, last]);
                delay += increment;
            }
        }
        // boxes
        else {
            while(i < numSlices) {
                for(row = 0; row < numRows; row++) {
                    if(row === 0) {
                        startCol++;
                        col = startCol;
                    }
                    if(col > -1 && col < numCols) {
                        i++;
                        clone = Y.clone(props);

                        // boxesGrow
                        if(this._type == 'boxesGrow') {
                            clone.height = parseFloat(slices[row][col].getComputedStyle('height'), 10) + 'px';
                            clone.width = parseFloat(slices[row][col].getComputedStyle('width'), 10) + 'px';
                            slices[row][col].setStyle('height', '0px');
                            slices[row][col].setStyle('width', '0px');
                            increment = 50;
                        }

                        last = i == numSlices - 1 ? true : false;
                        Y.later(delay, this, this._transitionSlice, [slices[row][col], clone, last]);
                    }
                    col--;
                }
                delay += increment;
            }
        }

        this._transitionSlicesFadeLast(delay);
	},

	/**
	 * Renders the divs for slice based transitions.
	 *
	 * @method _renderSlices
	 * @protected
	 */
	_renderSlices: function(numRows, numCols, multidimensional)
	{
        var itemIn      = this.get('itemIn'),
            itemHeight  = parseFloat(itemIn.getComputedStyle('height'), 10),
            itemWidth   = parseFloat(itemIn.getComputedStyle('width'), 10),
            img         = itemIn.one('img'),
            imgSrc      = img.get('src'),
            imgHeight   = parseFloat(img.getComputedStyle('height'), 10),
            imgWidth    = parseFloat(img.getComputedStyle('width'), 10),
            imgLeft     = parseFloat(img.getComputedStyle('left'), 10),
            imgTop      = parseFloat(img.getComputedStyle('top'), 10),
            col         = 0,
            row         = 0,
            sliceHeight = Math.round(itemHeight/numRows),
            sliceWidth  = Math.round(itemWidth/numCols),
            slice       = null,
            sliceImg    = null,
            slices      = [];

        for( ; row < numRows; row++) {

            if(typeof multidimensional !== 'undefined' && multidimensional) {
                slices[row] = [];
            }
            for(col = 0; col < numCols; col++) {

                slice = Y.Node.create('<div class="fl-slideshow-transition-slice"></div>');
                sliceImg = Y.Node.create('<img src="'+ imgSrc +'" />');

                slice.setStyles({
                    left: (sliceWidth * col) + 'px',
                    top: (sliceHeight * row) + 'px',
                    width: col == numCols - 1 ? (itemWidth - (sliceWidth * col)) + 'px' : sliceWidth + 'px',
                    height: row == numRows - 1 ? (itemHeight - (sliceHeight * row)) + 'px' : sliceHeight + 'px',
                    opacity: 0
                });

                sliceImg.setStyles({
                    height: imgHeight + 'px',
                    width: imgWidth + 'px',
                    top: imgTop - ((sliceHeight + (row * sliceHeight)) - sliceHeight) + 'px',
                    left: imgLeft - ((sliceWidth + (col * sliceWidth)) - sliceWidth) + 'px'
                });

                slice.append(sliceImg);
                itemIn.append(slice);

                if(typeof multidimensional !== 'undefined' && multidimensional) {
                    slices[row].push(slice);
                }
                else {
                    slices.push(slice);
                }
            }
        }

        return slices;
	},

	/**
	 * Fade the itemOut node.
	 *
	 * @method _transitionSlicesFadeLast
	 * @protected
	 */
	_transitionSlicesFadeLast: function(delay)
	{
		var itemOut = this.get('itemOut'),
			width = parseInt(Y.one('body').get('winWidth'), 10),
			delay = (width > FLBuilderLayoutConfig.breakpoints.medium) ? delay/1000 : 0;

        if(itemOut && !itemOut.hasClass('fl-slideshow-image-cropped')) {
			itemOut.transition({
                duration: delay + this.get('duration'),
                opacity: 0
			});
		}
	},

	/**
	 * Transitions a single slice.
	 *
	 * @method _transitionSlice
	 * @protected
	 */
	_transitionSlice: function(slice, props, last)
	{
        var callback = last ? Y.bind(this._transitionSlicesComplete, this) : null;

        slice.transition(props, callback);
	},

	/**
	 * Complete callback for slice based transitions.
	 *
	 * @method _transitionSlicesComplete
	 * @protected
	 */
	_transitionSlicesComplete: function()
	{
        var itemIn = this.get('itemIn');

        itemIn.all('.fl-slideshow-transition-slice').remove();
        itemIn.one('.fl-slideshow-image-img').setStyle('opacity', 1);
        this._transitionComplete();
	},

	/**
	 * Randomizes a slices array.
	 *
	 * @method _radomizeSlices
	 * @protected
	 */
	_randomizeSlices: function(slices)
	{
        var i = slices.length, j, temp;

        if(i === 0) {
            return;
        }
        while(--i) {
            j = Math.floor( Math.random() * ( i + 1 ) );
            temp = slices[i];
            slices[i] = slices[j];
            slices[j] = temp;
        }

        return slices;
	},

	_transitionKenBurns: function()
	{
        var kbDuration  = this.get('kenBurnsDuration'),
            duration    = this.get('duration'),
            itemIn      = this.get('itemIn'),
            zoom        = this.get('kenBurnsZoom');

        this._transitionFade();

        (new Y.FL.SlideshowKenBurns({
            duration: kbDuration + duration + 4,
            image: itemIn,
            zoom: zoom
        })).run();
	}

}, {

	/**
	 * Static property used to define the default attribute configuration of
	 * the Widget.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

		/**
		 * The Node to transition in.
		 *
		 * @attribute itemIn
		 * @type Node
		 * @default null
		 */
		itemIn: {
			value: null
		},

		/**
		 * The Node to transition out.
		 *
		 * @attribute itemOut
		 * @type Node
		 * @default null
		 */
		itemOut: {
			value: null
		},

		/**
		 * The duration of the transition in seconds.
		 *
		 * @attribute duration
		 * @type Number
		 * @default 0.5
		 */
		duration: {
			value: 0.5
		},

		/**
		 * The type of easing to use.
		 *
		 * @attribute easing
		 * @type String
		 * @default ease-out
		 */
		easing: {
			value: 'ease-out'
		},

		/**
		 * The type of transition to use.
		 *
		 * @attribute type
		 * @type String
		 * @default fade
		 */
		type: {
			value: 'fade'
		},

		/**
		 * The number of bars to use for
		 * transitions such as blinds.
		 *
		 * @attribute bars
		 * @type Number
		 * @default 15
		 */
		bars: {
			value: 15
		},

		/**
		 * The number of columns to use for
		 * transitions such as boxes.
		 *
		 * @attribute boxCols
		 * @type Number
		 * @default 8
		 */
		boxCols: {
			value: 8
		},

		/**
		 * The number of rows to use for
		 * transitions such as boxes.
		 *
		 * @attribute boxRows
		 * @type Number
		 * @default 4
		 */
		boxRows: {
			value: 4
		},

		/**
		 * The duration the ken burns effect will
		 * last, measured in seconds.
		 *
		 * @attribute kenBurnsDuration
		 * @type Number
		 * @default 4
		 */
		kenBurnsDuration: {
            value: 4
		},

		/**
		 * The amount of zoom to use for the Ken Burns effect.
		 *
		 * @attribute kenBurnsZoom
		 * @type Number
		 * @default 1.2
		 */
		kenBurnsZoom: {
            value: 1.2
		}
	},

	/**
	 * The types of transitions and associated functions.
	 *
	 * @property TYPES
	 * @type Object
	 * @readOnly
	 * @protected
	 * @static
	 */
	TYPES: {
		fade: '_transitionFade',
		none: '_transitionNone',
		slideLeft: '_transitionSlideLeft',
		slideRight: '_transitionSlideRight',
		slideUp: '_transitionSlideUp',
		slideDown: '_transitionSlideDown',
		blinds: '_transitionBars',
		bars: '_transitionBars',
		barsRandom: '_transitionBars',
		boxes: '_transitionBoxes',
		boxesRandom: '_transitionBoxes',
		boxesGrow: '_transitionBoxes',
		kenBurns: '_transitionKenBurns'
	},

	/**
	 * The types of transitions that can only be
	 * run on FL.SlideshowImage widgets.
	 *
	 * @property SLIDESHOW_IMAGE_TYPES
	 * @type Object
	 * @readOnly
	 * @protected
	 * @static
	 */
	SLIDESHOW_IMAGE_TYPES: [
        'blinds',
        'bars',
        'barsRandom',
        'boxes',
        'boxesRandom',
        'boxesGrow',
        'kenBurns'
	]
});

/**
 * A highly configurable slideshow widget.
 *
 * @namespace FL
 * @class Slideshow
 * @constructor
 * @param config {Object} Configuration object
 * @extends FL.SlideshowBase
 */
Y.namespace('FL').Slideshow = Y.Base.create('fl-slideshow', Y.FL.SlideshowBase, [], {

	/**
	 * A FL.SlideshowFrame instance used for the main image.
	 *
	 * @property frame
	 * @type FL.SlideshowFrame
	 * @default null
	 */
	frame: null,

	/**
	 * A FL.SlideshowNav instance used for the main nav.
	 *
	 * @property nav
	 * @type FL.SlideshowNav
	 * @default null
	 */
	nav: null,

	/**
	 * A FL.SlideshowNav instance used for the image nav's left button.
	 *
	 * @property imageNavLeft
	 * @type FL.SlideshowNav
	 * @default null
	 */
	imageNavLeft: null,

	/**
	 * A FL.SlideshowNav instance used for the image nav's right button.
	 *
	 * @property imageNavRight
	 * @type FL.SlideshowNav
	 * @default null
	 */
	imageNavRight: null,

	/**
	 * A FL.SlideshowThumbs instance used for the thumbnail grid.
	 *
	 * @property thumbs
	 * @type FL.SlideshowThumbs
	 * @default null
	 */
	thumbs: null,

	/**
	 * A FL.SlideshowThumbs instance used for the vertical thumbnail grid.
	 *
	 * @property verticalThumbs
	 * @type FL.SlideshowThumbs
	 * @default null
	 */
	verticalThumbs: null,

	/**
	 * A FL.SlideshowCaption instance.
	 *
	 * @property caption
	 * @type FL.SlideshowCaption
	 * @default null
	 */
	caption: null,

	/**
	 * A FL.SlideshowSocial instance.
	 *
	 * @property social
	 * @type FL.SlideshowSocial
	 * @default null
	 */
	social: null,

	/**
	 * A FL.SlideshowImage instance used to preload
	 * the next image.
	 *
	 * @property _nextImagePreloader
	 * @type FL.SlideshowImage
	 * @default null
	 * @protected
	 */
	_nextImagePreloader: null,

	/**
	 * An object that holds the initial nav settings
	 * when the mini nav has been enabled for a responsive layout.
	 *
	 * @property _initialNavSettings
	 * @type Object
	 * @default null
	 * @protected
	 */
	_initialNavSettings: null,

	/**
	 * Initializes the preloaders, nav buttons, fullscreen and captions.
	 *
	 * @method initializer
	 * @protected
	 */
	initializer: function()
	{
		// Preloader config
		var imageConfig = {
			loadGroup: 'main-preload',
			crop: this.get('crop'),
			position: this.get('position'),
			protect: this.get('protect'),
			upsize: this.get('upsize')
		};

		// Preloader
		this._nextImagePreloader = new Y.FL.SlideshowImage(imageConfig);

		// Nav buttons not needed for touch
		if(this._isMobile()) {
			this._removeNavButton('prevPage');
			this._removeNavButton('nextPage');
			this._removeNavButton('fullscreen');
		}

		// Fullscreen
		if(this._hasNavButton('fullscreen')) {
			if(Y.FL.SlideshowFullscreen.OS_SUPPORT) {
				this.plug(Y.FL.SlideshowFullscreen);
			}
			else {
				this._removeNavButton('fullscreen');
			}
		}
	},

	/**
	 * Calls the FL.SlideshowBase superclass renderUI method
	 * and renders the child widgets.
	 *
	 * @method renderUI
	 * @protected
	 */
	renderUI: function()
	{
		Y.FL.Slideshow.superclass.renderUI.apply(this, arguments);

		this._renderFrame();
		this._renderVerticalThumbs();
		this._renderNavAndThumbs();
		this._renderImageNav();
		this._renderMouseNav();
		this._renderCaption();
		this._renderSocial();
	},

	/**
	 * Calls the FL.SlideshowBase superclass bindUI method, binds
	 * _resizeChildWidgets to fire after the resize method inherited
	 * from FL.SlideshowBase, shows the loading image, binds overlay events
	 * and binds an event to load an image into the frame.
	 *
	 * @method bindUI
	 * @protected
	 */
	bindUI: function()
	{
		var ssBB 			= this.get('boundingBox'),
			frameBB         = this.frame.get('boundingBox'),
			navOverlay		= this.get('navOverlay'),
			navType			= this.get('navType'),
			nav 			= this._getNav(),
			clickAction     = this.get('clickAction');

		// Call superclass bindUI
		Y.FL.Slideshow.superclass.bindUI.apply(this, arguments);

		// Resize child widgets after the superclass resize method.
		Y.Do.after(this._resizeChildWidgets, this, 'resize');

		// Loading events
		this.on('albumLoadStart', this._albumLoadStart, this);
		this.on('albumLoadComplete', this._albumLoadComplete, this);
		this.on('imageLoadComplete', this._loadFrame, this);

		// Loading image
		if(this.get('loadingImageAlwaysEnabled')) {
			this.frame.on('transitionInit', Y.bind(this._showLoadingImageWithDelay, this));
			this.frame.on('transitionStart', Y.bind(this._hideLoadingImage, this));
		}

		// Overlay events
		if(this.get('overlayHideOnMousemove')) {
			if(nav && navOverlay) {
				this.frame.once('transitionComplete', nav.slideshowOverlay.hideWithTimer, nav.slideshowOverlay);
				ssBB.on('mousemove', Y.bind(this._toggleNav, this));
			}
			if(navType == 'buttons' || navType == 'thumbs' || navType == 'custom') {
				ssBB.on('mouseenter', Y.bind(this._checkOverlaysOnMouseenter, this));
				ssBB.on('mouseleave', Y.bind(this._hideAllOverlays, this));
			}
		}

		ssBB.delegate('click', Y.bind(this._overlayCloseClick, this), '.fl-slideshow-overlay-close');

		// Click action
		if(clickAction == 'gallery' || clickAction == 'url') {
            frameBB.delegate('click', Y.bind(this._frameClick, this), '.fl-slideshow-image-img');
		}
	},

	/**
	 * Calls the FL.SlideshowBase superclass syncUI method
	 * and makes the bounding box unselectable.
	 *
	 * @method syncUI
	 * @protected
	 */
	syncUI: function()
	{
		var bb = this.get('boundingBox');

		Y.FL.Slideshow.superclass.syncUI.apply(this, arguments);

		bb._node.onselectstart = function() { return false; };
		bb._node.unselectable = "on";
		bb._node.style.MozUserSelect = "none";

		if(this.get('clickAction') != 'none') {
            this.frame.get('boundingBox').addClass('fl-click-action-enabled');
		}
	},

	/**
	 * Checks to see if the current device is mobile.
	 *
	 * @since 1.9.3
	 * @access private
	 * @method _isMobile
	 * @return {Boolean}
	 */
	_isMobile: function()
	{
	    return /Mobile|Android|Silk\/|Kindle|BlackBerry|Opera Mini|Opera Mobi|webOS/i.test( navigator.userAgent );
	},

	/**
	 * Unload all slideshow images and pause
	 * the slideshow.
	 *
	 * @method unload
	 */
	unload: function()
	{
		this.pause();
		this.frame.unload();

		if(this.thumbs !== null) {
			this.thumbs.unload();
		}
	},

	/**
	 * @method _albumLoadStart
	 * @protected
	 */
	_albumLoadStart: function()
	{
		this._showLoadingImage();
	},

	/**
	 * @method _albumLoadComplete
	 * @protected
	 */
	_albumLoadComplete: function()
	{
		this.frame.once('transitionStart', Y.bind(this._hideLoadingImage, this));
	},

	/**
	 * Resizes all enabled child widgets.
	 *
	 * @method _resizeChildWidgets
	 * @protected
	 */
	_resizeChildWidgets: function()
	{
		var bb				= this.get('boundingBox'),
			cb				= this.get('contentBox'),
			imageNavEnabled = this.get('imageNavEnabled');

		this._renderNavAndThumbs();

		if(this.get('verticalThumbsOverlay')) {
			this._resizeFrame(cb.get('offsetWidth'), bb.get('offsetHeight'));
			this._resizeVerticalThumbs();
		}
		else {
			this._resizeVerticalThumbs();
			this._resizeFrame(cb.get('offsetWidth'), bb.get('offsetHeight'));
		}

		if(imageNavEnabled) {
			this._positionImageNav();
		}

		this._positionLoadingImage();
	},

	/**
	 * @method _renderVerticalThumbs
	 * @protected
	 */
	_renderVerticalThumbs: function()
	{
		var threshold 	= this.get('responsiveThreshold'),
			ssBB 		= this.get('boundingBox'),
			bbWidth 	= ssBB.get('offsetWidth'),
			vtBB;

		if(this.get('verticalThumbsEnabled') && bbWidth > threshold) {

			this.verticalThumbs = new Y.FL.SlideshowThumbs(this._getVerticalThumbsConfig());
			this.add(this.verticalThumbs);
			this.verticalThumbs.render(ssBB);

			vtBB = this.verticalThumbs.get('boundingBox');
			vtBB.addClass('fl-slideshow-vertical-thumbs');
			vtBB.setStyle(this.get('verticalThumbsPosition'), 0);
			ssBB.append(vtBB);

			if(this.get('verticalThumbsOverlay')) {
				this.verticalThumbs.plug(Y.FL.SlideshowOverlay, {
					hideDelay: this.get('overlayHideDelay'),
					hideStyle: 'left'
				});

				this.frame.get('boundingBox').append(vtBB);
				this.verticalThumbs.resize();
			}
			else {
				this.verticalThumbs.resize();
				this._adjustContentForVerticalThumbs();
			}

			this._bindVerticalThumbs();
		}
	},

	/**
	 * Prepares and returns the vertical thumbs config object.
	 *
	 * @method _getVerticalThumbsConfig
	 * @protected
	 * @returns Object
	 */
	_getVerticalThumbsConfig: function()
	{
		var attrs = this.getAttrs(),
			config = {
				columns: 				attrs.verticalThumbsColumns,
				rows: 					'auto',
				centerSinglePage: 		false,
				horizontalSpacing: 		attrs.verticalThumbsHorizontalSpacing,
				verticalSpacing:	 	attrs.verticalThumbsVerticalSpacing,
				spaceEvenly: 			attrs.verticalThumbsSpaceEvenly,
				rightNavEnabled: 		false,
				leftNavEnabled: 		false,
				topNavEnabled: 			attrs.verticalThumbsTopNavEnabled,
				topNavButtons:			attrs.verticalThumbsTopNavButtons,
				bottomNavEnabled: 		attrs.verticalThumbsBottomNavEnabled,
				bottomNavButtons:		attrs.verticalThumbsBottomNavButtons,
				pauseOnClick: 			attrs.verticalThumbsPauseOnClick,
				transition:				attrs.verticalThumbsTransition,
				transitionDirection: 	attrs.verticalThumbsTransitionDirection,
				transitionEasing:		attrs.verticalThumbsTransitionEasing,
				touchSupport:			true,
				imageConfig: {
					crop: 				attrs.verticalThumbsImageCrop,
					width: 				attrs.verticalThumbsImageWidth,
					height: 			attrs.verticalThumbsImageHeight
				}
			};

		return config;
	},

	_bindVerticalThumbs: function()
	{
		var ssBB 		= this.get('boundingBox'),
			hideOnMouse = this.get('overlayHideOnMousemove'),
			vtOverlay 	= this.get('verticalThumbsOverlay'),
			vt			= this.verticalThumbs;

		if(vt && hideOnMouse && vtOverlay) {
			this.frame.once('transitionComplete', vt.slideshowOverlay.hideWithTimer, vt.slideshowOverlay);
			ssBB.on('mousemove', Y.bind(this._toggleVerticalThumbs, this));
			ssBB.on('mouseenter', Y.bind(this._toggleVerticalThumbs, this));
		}
	},

	/**
	 * Resizes the vertical thumbs.
	 *
	 * @method _resizeVerticalThumbs
	 * @protected
	 */
	_resizeVerticalThumbs: function()
	{
		var vtEnabled = this.get('verticalThumbsEnabled'),
			vtOverlay,
			threshold,
			ssBB,
			bbWidth,
			navOverlay,
			navType,
			nav,
			navBB;

		if(vtEnabled) {
			vtOverlay	= this.get('verticalThumbsOverlay');
			threshold 	= this.get('responsiveThreshold');
			ssBB 		= this.get('boundingBox');
			bbWidth 	= ssBB.get('offsetWidth');
			navOverlay	= this.get('navOverlay');
			navType		= this.get('navType');
			nav			= this._getNav();

			if(this.verticalThumbs && bbWidth > threshold) {

				this.verticalThumbs.get('boundingBox').setStyle('display', 'block');
				this.verticalThumbs.resize();

				if(!vtOverlay) {
					this._adjustContentForVerticalThumbs();
				}
				else if(nav && navOverlay) {

					navBB = nav.get('boundingBox');

					if(navType == 'thumbs') {
						this._adjustOverlayForVerticalThumbs(navBB, true);
						this.thumbs.resize();
					}
					else {
						this._adjustOverlayForVerticalThumbs(navBB);
					}
				}
			}
			else if(!this.verticalThumbs && bbWidth > threshold) {
				this._renderVerticalThumbs();
			}
			else if(this.verticalThumbs && bbWidth <= threshold) {

				this.verticalThumbs.get('boundingBox').setStyle('display', 'none');

				if(!vtOverlay) {
					this.get('contentBox').setStyles({
						left: 'auto',
						position: 'relative',
						right: 'auto',
						width: 'auto'
					});
				}
			}
		}
	},

	/**
	 * Toggles the visibility of the vertical thumbs.
	 *
	 * @method _toggleVerticalThumbs
	 * @protected
	 */
	_toggleVerticalThumbs: function()
	{
		if(this.verticalThumbs) {
			if(this.verticalThumbs.slideshowOverlay._visible) {
				this.verticalThumbs.slideshowOverlay.hideWithTimer();
			}
			else {
				this.verticalThumbs.slideshowOverlay.show();
			}
		}
	},

	/**
	 * Adjusts the content position and width
	 * for the vertical thumbs.
	 *
	 * @method _adjustContentForVerticalThumbs
	 * @protected
	 */
	_adjustContentForVerticalThumbs: function()
	{
		var ssBB 	= this.get('boundingBox'),
			vtBB 	= this.verticalThumbs.get('boundingBox'),
			vtPos 	= this.get('verticalThumbsPosition'),
			ssCB 	= this.get('contentBox'),
			cbPos 	= vtPos == 'left' ? 'right' : 'left',
			cbWidth = ssBB.get('offsetWidth') - vtBB.get('offsetWidth');

		ssCB.setStyle('position', 'absolute');
		ssCB.setStyle(cbPos, 0);
		ssCB.setStyle('width', cbWidth);
	},

	/**
	 * Adjusts an overlay's position for the vertical
	 * thumbs when they are overlaid as well.
	 *
	 * @method _adjustOverlayForVerticalThumbs
	 * @protected
	 */
	_adjustOverlayForVerticalThumbs: function(node, useMargin)
	{
		var vtEnabled 	= this.get('verticalThumbsEnabled'),
			vtOverlay 	= this.get('verticalThumbsOverlay'),
			vtBB		= null,
			vtPos		= null,
			margin		= typeof useMargin === 'undefined' ? '' : 'margin-',
			vtWidth		= 0;

		if(this.verticalThumbs && vtEnabled && vtOverlay) {
			vtBB = this.verticalThumbs.get('boundingBox');
			vtWidth = vtBB.get('offsetWidth');
			vtPos = this.get('verticalThumbsPosition');

			if(vtPos == 'left') {
				node.setStyle(margin + 'left', vtWidth + 'px');
			}
			else {
				node.setStyle(margin + 'right', vtWidth + 'px');
			}
		}
	},

	/**
	 * Creates and renders a new instance of FL.SlideshowFrame
	 * used for the main image.
	 *
	 * @method _renderFrame
	 * @protected
	 */
	_renderFrame: function()
	{
		this.frame = new Y.FL.SlideshowFrame({
			imageConfig: {
				loadGroup: 'main',
				loadPriority: true,
				crop: this.get('crop'),
				cropHorizontalsOnly: this.get('cropHorizontalsOnly'),
				position: this.get('position'),
				protect: this.get('protect'),
				upsize: this.get('upsize'),
				showVideoButton: this.get('navOverlay')
			},
			touchSupport: this.get('touchSupport')
		});

		this.add(this.frame);
		this.frame.render(this.get('contentBox'));
		this.frame.get('boundingBox').addClass('fl-slideshow-main-image');
		this._setPlayingTimerEvent(this.frame, 'transitionComplete');
		this._loadingImageContainer = this.frame.get('contentBox');
	},

	/**
	 * Resizes the frame used for the main image.
	 *
	 * @method _resizeMainImage
	 * @param width {Number} The width to resize to.
	 * @param height {Number} The height to resize to.
	 * @protected
	 */
	_resizeFrame: function(width, height)
	{
		var navOverlay	= this.get('navOverlay'),
			nav 		= this._getNav();

		if(nav && !navOverlay) {
			height -= parseInt(nav.get('boundingBox').getComputedStyle('height'), 10);
		}

		this.frame.resize(width, height);
	},

	/**
	 * Called when the imageLoadComplete event fires.
	 * Loads an image into the frame and preloads the next image.
	 *
	 * @method _loadFrame
	 * @param e {Object} Event object containing the image info.
	 * @protected
	 */
	_loadFrame: function(e)
	{
		var activeIndex		= this.imageInfo.index,
			images			= this.albumInfo.images,
			nextIndex 		= activeIndex + 1 >= images.length ? 0 : activeIndex + 1,
			width			= this.frame.get('width'),
			height			= this.frame.get('height');

		// Load the frame.
		this.frame.load(e.imageInfo);

		// Remove main preload images from the load queue.
		Y.FL.SlideshowImageLoader.removeGroup('main-preload');

		// Preload the next image.
		this._nextImagePreloader.preload(images[nextIndex], width, height);
	},

	/**
	 * Fired when the frame img tag is clicked.
	 *
	 * @method _frameClick
	 * @protected
	 */
	_frameClick: function()
	{
        var clickAction    = this.get('clickAction'),
            clickActionUrl = this.get('clickActionUrl');

        if(clickAction == 'url') {
            window.location.href = clickActionUrl;
        }
        else if(clickAction == 'gallery') {
            window.location.href = this.imageInfo.link;
        }
	},

	/**
	 * Sets attributes to display a compact nav
	 * for responsive layouts.
	 *
	 * @method _initMiniNav
	 * @protected
	 */
	_initMiniNav: function()
	{
		var buttons = [];

		if(this._hasNavButton('prev')) {
			buttons.push('prev');
		}
		if(this._hasNavButton('thumbs') || this.get('navType') == 'thumbs') {
			buttons.push('thumbs');
		}
		if(this._hasNavButton('caption')) {
			buttons.push('caption');
		}
		if(this._hasNavButton('social')) {
			buttons.push('social');
		}
		if(this._hasNavButton('buy')) {
			buttons.push('buy');
		}
		if(this._hasNavButton('play')) {
			buttons.push('play');
		}
		if(this._hasNavButton('fullscreen') && !('ontouchstart' in window)) {
			buttons.push('fullscreen');
		}
		if(this._hasNavButton('next')) {
			buttons.push('next');
		}

		this._initialNavSettings = {
			buttons: this.get('navButtons'),
			buttonsLeft: this.get('navButtonsLeft'),
			buttonsRight: this.get('navButtonsRight'),
			type: this.get('navType')
		};

		this._set('navButtons', buttons);
		this._set('navButtonsLeft', []);
		this._set('navButtonsRight', []);
		this._set('navType', 'buttons');
	},

	/**
	 * Renders the nav and thumbs layout based on the
	 * current window size.
	 *
	 * @method _renderNavAndThumbs
	 * @protected
	 */
	_renderNavAndThumbs: function()
	{
		var navType		= this.get('navType'),
			renderNav	= false,
			bbWidth,
			threshold;

		if(navType == 'buttons' || navType == 'thumbs') {
			bbWidth		= this.get('boundingBox').get('offsetWidth');
			threshold	= this.get('responsiveThreshold');

			if(bbWidth <= threshold && this._initialNavSettings === null) {
				this._initMiniNav();
				renderNav = true;
			}
			else if(bbWidth > threshold && this._initialNavSettings !== null) {
				this._set('navButtons', this._initialNavSettings.buttons);
				this._set('navButtonsLeft', this._initialNavSettings.buttonsLeft);
				this._set('navButtonsRight', this._initialNavSettings.buttonsRight);
				this._set('navType', this._initialNavSettings.type);
				this._initialNavSettings = null;
				renderNav = true;
			}

			// Button nav
			if(renderNav || this.nav === null) {
				this._renderNav();
			}

			// Thumbs nav
			if(renderNav || this.thumbs === null) {
				this._renderThumbs();
			}
			else if(this._thumbsEnabled()) {
				this._resizeThumbs();
			}

			// Caption
			if(renderNav && this.caption !== null) {
				this._syncCaption();
			}

			// Social
			if(renderNav && this.social !== null) {
				this._syncSocial();
			}
		}
	},

	/**
	 * Creates and renders a new instance of FL.SlideshowNav
	 * used for the main nav.
	 *
	 * @method _renderNav
	 * @protected
	 */
	_renderNav: function()
	{
		var frameBB		= this.frame.get('boundingBox'),
			navBB 		= null,
			navOverlay	= this.get('navOverlay'),
			navPosition = this.get('navPosition');

		// Destroy old instances
		this._destroyNav();

		// Create a new instance
		if(this.get('navType') == 'buttons') {

			// Create the nav
			this.nav = new Y.FL.SlideshowNav({
				buttons: this.get('navButtons'),
				buttonsLeft: this.get('navButtonsLeft'),
				buttonsRight: this.get('navButtonsRight')
			});

			// Add to widget parent and render
			this.add(this.nav);
			this.nav.render(this.get('contentBox'));
			navBB = this.nav.get('boundingBox');

			// Plug overlay?
			if(navOverlay) {
				this.nav.plug(Y.FL.SlideshowOverlay, {
					hideDelay: this.get('overlayHideDelay')
				});

				navBB.setStyle('position', 'absolute');
				navBB.setStyle(navPosition, '0px');
			}

			// Insert
			if(navPosition == 'top') {
				frameBB.insert(navBB, 'before');
			}
			else {
				frameBB.insert(navBB, 'after');
			}

			// CSS class name
			navBB.addClass('fl-slideshow-main-nav');
		}
	},

	/**
	 * Destroy the current nav instance.
	 *
	 * @method _destroyNav
	 * @protected
	 */
	_destroyNav: function()
	{
        if(this.nav !== null) {
			if(this.nav.slideshowOverlay) {
				this.nav.slideshowOverlay.destroy();
			}

            this.nav.get('boundingBox').remove();
			this.remove(this.nav);
			try { this.nav.destroy(true); } catch(e) {}
			this.nav = null;
		}
	},

	/**
	 * Returns the nav object or null if navType is
	 * set to none or custom.
	 *
	 * @method _getNav
	 * @protected
	 */
	_getNav: function()
	{
		var navType = this.get('navType');

		if(navType == 'buttons') {
			return this.nav;
		}
		else if(navType == 'thumbs') {
			return this.thumbs;
		}
		else {
			return null;
		}
	},

	/**
	 * Toggles the visibility of the nav or thumbs nav
	 * if navOverlay is set to true.
	 *
	 * @method _toggleNav
	 * @protected
	 */
	_toggleNav: function()
	{
		var nav = this._getNav();

		if(nav.slideshowOverlay) {
			if(nav.slideshowOverlay._visible) {
				nav.slideshowOverlay.hideWithTimer();
			}
			else {
				nav.slideshowOverlay.show();
			}
		}
	},

	/**
	 * Creates and renders two instances of FL.SlideshowNav for the
	 * prev and next button that will be overlaid on the main image.
	 *
	 * @method _renderImageNav
	 * @protected
	 */
	_renderImageNav: function()
	{
		var ssBB;

		if(this.get('imageNavEnabled')) {
			if(this._isMobile()) {
				this._set('imageNavEnabled', false);
			}
			else {
				ssBB = this.get('boundingBox');

				this.imageNavLeft = new Y.FL.SlideshowNav({
				    buttons: ['prev'],
				    useFontIcons: false
				});

				this.imageNavRight = new Y.FL.SlideshowNav({
				    buttons: ['next'],
				    useFontIcons: false
				});

				this.add(this.imageNavLeft);
				this.add(this.imageNavRight);

				this.imageNavLeft.render(this.frame.get('boundingBox'));
				this.imageNavRight.render(this.frame.get('boundingBox'));

				this.imageNavLeft.plug(Y.FL.SlideshowOverlay, { hideDelay: this.get('overlayHideDelay') });
				this.imageNavRight.plug(Y.FL.SlideshowOverlay, { hideDelay: this.get('overlayHideDelay') });

				if(this.get('overlayHideOnMousemove')) {
					this.frame.once('transitionComplete', this.imageNavLeft.slideshowOverlay.hideWithTimer, this.imageNavLeft.slideshowOverlay);
					this.frame.once('transitionComplete', this.imageNavRight.slideshowOverlay.hideWithTimer, this.imageNavRight.slideshowOverlay);
					ssBB.on('mousemove', Y.bind(this._toggleImageNav, this));
					ssBB.on('mouseenter', Y.bind(this._toggleImageNav, this));
				}

				this.imageNavLeft.get('boundingBox').addClass('fl-slideshow-image-nav-left');
				this.imageNavRight.get('boundingBox').addClass('fl-slideshow-image-nav-right');
			}
		}
	},

	/**
	 * @method _positionImageNav
	 * @protected
	 */
	_positionImageNav: function()
	{
		var leftBB			= this.imageNavLeft.get('boundingBox'),
			rightBB			= this.imageNavRight.get('boundingBox'),
			imageNavHeight 	= leftBB.get('offsetHeight'),
			frameHeight 	= this.frame.get('boundingBox').get('offsetHeight'),
			top 			= frameHeight/2 - imageNavHeight/2,
			styles          = {
                top: top + 'px',
                display: 'block'
			};

		leftBB.setStyles(styles);
		rightBB.setStyles(styles);

		this._adjustOverlayForVerticalThumbs(leftBB);
		this._adjustOverlayForVerticalThumbs(rightBB);
	},

	/**
	 * Toggles the visibility of the image nav buttons.
	 *
	 * @method _toggleImageNav
	 * @protected
	 */
	_toggleImageNav: function()
	{
		if(this.imageNavLeft.slideshowOverlay._visible) {
			this.imageNavLeft.slideshowOverlay.hideWithTimer();
		}
		else {
			this.imageNavLeft.slideshowOverlay.show();
		}

		if(this.imageNavRight.slideshowOverlay._visible) {
			this.imageNavRight.slideshowOverlay.hideWithTimer();
		}
		else {
			this.imageNavRight.slideshowOverlay.show();
		}
	},

	/**
	 * @method _renderMouseNav
	 * @protected
	 */
	_renderMouseNav: function()
	{
		if(this.get('mouseNavEnabled') && !('ontouchstart' in window) && !window.navigator.msPointerEnabled) {
			this.plug(Y.FL.SlideshowMouseNav, {
				trigger: this.frame.get('boundingBox')
			});
		}
	},

	/**
	 * Checks whether the thumbs are enabled.
	 *
	 * @method _thumbsEnabled
	 * @protected
	 * @returns Boolean
	 */
	_thumbsEnabled: function()
	{
		var navType = this.get('navType');

		if(navType == 'thumbs') {
			return true;
		}
		if((navType == 'buttons' || navType == 'custom') && this._hasNavButton('thumbs')) {
			return true;
		}
		else {
			return false;
		}
	},

	/**
	 * Creates and renders a new instance of FL.SlideshowThumbs.
	 *
	 * @method _renderThumbs
	 * @protected
	 */
	_renderThumbs: function()
	{
		var frameBB, navOverlay, navPosition, navType;

		// Destroy old instances
		this._destroyThumbs();

		// Create a new instance
		if(this._thumbsEnabled()) {
			frameBB			= this.frame.get('boundingBox');
			navOverlay		= this.get('navOverlay');
			navPosition 	= this.get('navPosition');
			navType			= this.get('navType');

			// Create the thumbs
			this.thumbs = new Y.FL.SlideshowThumbs(this._getThumbsConfig());

			// This breaks sometimes on SM Next. Try/catch bandaid for now.
			try { this.add(this.thumbs); } catch(e) {}

			// Overlay setup
			if(navType == 'buttons' || navType == 'custom') {
				this.thumbs.plug(Y.FL.SlideshowOverlay, {
					hideDelay: this.get('overlayHideDelay'),
					hideStyle: 'left',
					visible: false
				});
			}
			else if(navType == 'thumbs' && navOverlay) {
				this.thumbs.plug(Y.FL.SlideshowOverlay, {
					hideDelay: this.get('overlayHideDelay'),
					hideStyle: 'left'
				});
			}

			// Insert
			this.thumbs.render(this.get('contentBox'));

			if(navPosition == 'top') {
				frameBB.insert(this.thumbs.get('boundingBox'), 'before');
			}
			else {
				frameBB.insert(this.thumbs.get('boundingBox'), 'after');
			}

			// Hide overlay thumbs on click
			if(this.get('thumbsHideOnClick') && navType != 'thumbs') {
				this.thumbs.on('imageClick', Y.bind(this._hideThumbsOnImageClick, this));
			}

			this._syncThumbs();
		}
	},

	/**
	 * Destroy the current thumbs instance.
	 *
	 * @method _destroyThumbs
	 * @protected
	 */
	_destroyThumbs: function()
	{
        if(this.thumbs !== null) {
			if(this.thumbs.slideshowOverlay) {
				this.thumbs.slideshowOverlay.destroy();
			}

            this.thumbs.get('boundingBox').remove();
			this.remove(this.thumbs);
			try { this.thumbs.destroy(true); } catch(e) {}
			this.thumbs = null;
		}
	},

	/**
	 * Syncs the thumbs UI styles.
	 *
	 * @method _syncThumbs
	 * @protected
	 */
	_syncThumbs: function()
	{
		var thumbsBB			= this.thumbs.get('boundingBox'),
			navOverlay			= this.get('navOverlay'),
			navPosition 		= this.get('navPosition'),
			navType				= this.get('navType'),
			paddingType			= 'padding' + navPosition.charAt(0).toUpperCase() + navPosition.slice(1),
			navHeight			= 0;

		if(navType == 'buttons') {
			navHeight = parseInt(this.nav.get('boundingBox').getComputedStyle('height'), 10);
			thumbsBB.setStyle('position', 'absolute');

			if(navOverlay) {
				thumbsBB.setStyle(paddingType, navHeight + 'px');
				thumbsBB.setStyle(navPosition, '0px');
			}
			else {
				thumbsBB.setStyle(navPosition, navHeight + 'px');
			}
		}
		if(navType == 'custom' || (navType == 'thumbs' && navOverlay)) {
			thumbsBB.setStyle('position', 'absolute');
			thumbsBB.setStyle(navPosition, '0px');
		}

		this.thumbs.resize();
	},

	/**
	 * Prepares and returns the thumbs config object.
	 *
	 * @method _getThumbsConfig
	 * @protected
	 * @returns Object
	 */
	_getThumbsConfig: function()
	{
		var attrs 					= this.getAttrs(),
			navType					= this.get('navType'),
			imageConfig 			= {
				crop: attrs.thumbsImageCrop,
				width: attrs.thumbsImageWidth,
				height: attrs.thumbsImageHeight
			},
			config 					= {
				columns: 				'auto',
				rows: 					1,
				horizontalSpacing: 		attrs.thumbsHorizontalSpacing,
				verticalSpacing:	 	attrs.thumbsVerticalSpacing,
				spaceEvenly: 			attrs.thumbsSpaceEvenly,
				centerSinglePage:	 	attrs.thumbsCenterSinglePage,
				pauseOnClick: 			attrs.thumbsPauseOnClick,
				transition:				attrs.thumbsTransition,
				transitionDirection: 	attrs.thumbsTransitionDirection,
				transitionEasing:		attrs.thumbsTransitionEasing,
				leftNavButtons: 		attrs.navButtonsLeft,
				rightNavButtons: 		attrs.navButtonsRight,
				imageConfig:			imageConfig,
				touchSupport:			true
			};

			if(navType == 'buttons' || navType == 'custom') {
				if('ontouchstart' in window) {
					config.leftNavEnabled = false;
					config.rightNavEnabled = false;
				}
				else {
					config.centerSinglePage = false;
					config.leftNavButtons 	= ['prevPage'];
					config.rightNavButtons 	= ['nextPage'];
				}
			}

			return config;
	},

	/**
	 * Resizes the thumbs.
	 *
	 * @method _resizeThumbs
	 * @protected
	 */
	_resizeThumbs: function()
	{
		if(this.thumbs) {
			this.thumbs.resize();
		}
	},

	/**
	 * Shows or hides the thumbs.
	 *
	 * @method _toggleThumbs
	 * @protected
	 */
	_toggleThumbs: function()
	{
		this._toggleOverlay(this.thumbs.slideshowOverlay);
	},

	/**
	 * Hides the thumbs when a thumb image is clicked.
	 *
	 * @method _hideThumbsOnImageClick
	 * @protected
	 */
	_hideThumbsOnImageClick: function()
	{
		if(this.thumbs.slideshowOverlay) {
			this.thumbs.slideshowOverlay._focus = false;
			this.thumbs.slideshowOverlay.enable();
			this.thumbs.slideshowOverlay.hide();

			if(this.nav && this.nav.slideshowOverlay) {
				this.nav.slideshowOverlay.enable();
			}
		}
	},

	/**
	 * Creates and renders a new instance of FL.SlideshowCaption.
	 *
	 * @method _renderCaption
	 * @protected
	 */
	_renderCaption: function()
	{
		if(this._hasNavButton('caption')) {

			this.caption = new Y.FL.SlideshowCaption({
				lessLinkText: this.get('captionLessLinkText'),
				moreLinkText: this.get('captionMoreLinkText'),
				textLength: this.get('captionTextLength'),
				stripTags: this.get('captionStripTags')
			});

			this.add(this.caption);

			this.caption.plug(Y.FL.SlideshowOverlay, {
				hideDelay: this.get('overlayHideDelay'),
				visible: false,
				closeButton: true
			});

			this._syncCaption();
		}
	},

	/**
	 * Syncs the caption UI styles.
	 *
	 * @method _syncCaption
	 * @protected
	 */
	_syncCaption: function()
	{
		var captionBB		= this.caption.get('boundingBox'),
			navOverlay		= this.get('navOverlay'),
			navPosition 	= this.get('navPosition'),
			nav 			= this._getNav(),
			paddingType		= 'padding' + navPosition.charAt(0).toUpperCase() + navPosition.slice(1),
			navHeight		= 0;

		captionBB.setStyle('position', 'absolute');

		if(nav) {
			navHeight = parseInt(nav.get('boundingBox').getComputedStyle('height'), 10);
		}

		if(nav && navOverlay) {
			captionBB.setStyle(paddingType, navHeight + 'px');
			captionBB.setStyle(navPosition, '0px');
		}
		else {
			captionBB.setStyle(navPosition, navHeight + 'px');
		}
	},

	/**
	 * Shows or hides the caption.
	 *
	 * @method _toggleCaption
	 * @protected
	 */
	_toggleCaption: function()
	{
		this._toggleOverlay(this.caption.slideshowOverlay);
	},

	/**
	 * Creates and renders a new instance of FL.SlideshowSocial.
	 *
	 * @method _renderSocial
	 * @protected
	 */
	_renderSocial: function()
	{
		if(this._hasNavButton('social')) {
			this.social = new Y.FL.SlideshowSocial();
			this.add(this.social);

			this.social.plug(Y.FL.SlideshowOverlay, {
				hideDelay: this.get('overlayHideDelay'),
				visible: false,
				closeButton: true
			});

			this._syncSocial();
		}
	},

	/**
	 * Syncs the social UI styles.
	 *
	 * @method _syncSocial
	 * @protected
	 */
	_syncSocial: function()
	{
		var socialBB		= this.social.get('boundingBox'),
			navOverlay		= this.get('navOverlay'),
			navPosition 	= this.get('navPosition'),
			nav 			= this._getNav(),
			paddingType		= 'padding' + navPosition.charAt(0).toUpperCase() + navPosition.slice(1),
			navHeight		= 0;

		socialBB.setStyle('position', 'absolute');

		if(nav) {
			navHeight = parseInt(nav.get('boundingBox').getComputedStyle('height'), 10);
		}
		if(nav && navOverlay) {
			socialBB.setStyle(paddingType, navHeight + 'px');
			socialBB.setStyle(navPosition, '0px');
		}
		else {
			socialBB.setStyle(navPosition, navHeight + 'px');
		}
	},

	/**
	 * Shows or hides the social buttons.
	 *
	 * @method _toggleSocial
	 * @protected
	 */
	_toggleSocial: function()
	{
		this._toggleOverlay(this.social.slideshowOverlay);
		// Refresh iframe to fix tweet button issue visibility inside hidden elements
		var iFrame = jQuery('.fl-slideshow-social-content').find('iframe');
		iFrame.remove();
		jQuery('.fl-slideshow-social-content').prepend(iFrame);
	},

	/**
	 * Shows or hides an overlaid widget based
	 * on its current visibility.
	 *
	 * @method _toggleOverlay
	 * @param overlay {Object} The overlay to toggle.
	 * @protected
	 */
	_toggleOverlay: function(overlay)
	{
		var navType	= this.get('navType'),
			nav 	= this._getNav();

		if(overlay._visible) {
			if(nav && nav.slideshowOverlay) {
				nav.slideshowOverlay.enable();
			}
			overlay.enable();
			overlay.hide();
		}
		else {
			if(nav && nav.slideshowOverlay) {
				nav.slideshowOverlay.disable();
			}
			overlay.show();
			overlay.disable();
		}

		if(this.thumbs && navType != 'thumbs' && this.thumbs.slideshowOverlay !== overlay) {
			this.thumbs.slideshowOverlay.enable();
			this.thumbs.slideshowOverlay.hide();
		}
		if(this.caption && this.caption.slideshowOverlay !== overlay) {
			this.caption.slideshowOverlay.enable();
			this.caption.slideshowOverlay.hide();
		}
		if(this.social && this.social.slideshowOverlay !== overlay) {
			this.social.slideshowOverlay.enable();
			this.social.slideshowOverlay.hide();
		}
	},

	/**
	 * Called when an overlay's close button is clicked.
	 *
	 * @method _overlayCloseClick
	 * @protected
	 */
	_overlayCloseClick: function()
	{
		if(this.nav && this.nav.slideshowOverlay) {
			this.nav.slideshowOverlay.enable();
		}
		if(this.thumbs && this.thumbs.slideshowOverlay) {
			this.thumbs.slideshowOverlay.enable();
		}
		if(this.caption) {
			this.caption.slideshowOverlay.enable();
		}
		if(this.social) {
			this.social.slideshowOverlay.enable();
		}
		if(this.imageNavLeft) {
			this.imageNavLeft.slideshowOverlay.enable();
			this.imageNavRight.slideshowOverlay.enable();
		}
	},

	/**
	 * Hides all overlaid widgets.
	 *
	 * @method _hideAllOverlays
	 * @protected
	 */
	_hideAllOverlays: function()
	{
		if(this.nav && this.nav.slideshowOverlay && this.nav.slideshowOverlay._visible) {
			this.nav.slideshowOverlay.enable();
			this.nav.slideshowOverlay.hideWithTimer();
		}
		if(this.thumbs && this.thumbs.slideshowOverlay && this.thumbs.slideshowOverlay._visible) {
			this.thumbs.slideshowOverlay.enable();
			this.thumbs.slideshowOverlay.hideWithTimer();
		}
		if(this.caption && this.caption.slideshowOverlay._visible) {
			this.caption.slideshowOverlay.enable();
			this.caption.slideshowOverlay.hideWithTimer();
		}
		if(this.social && this.social.slideshowOverlay._visible) {
			this.social.slideshowOverlay.enable();
			this.social.slideshowOverlay.hideWithTimer();
		}
		if(this.imageNavLeft) {
			this.imageNavLeft.slideshowOverlay.enable();
			this.imageNavLeft.slideshowOverlay.hideWithTimer();
			this.imageNavRight.slideshowOverlay.enable();
			this.imageNavRight.slideshowOverlay.hideWithTimer();
		}
	},

	/**
	 * Checks if overlays are still visible when the mouse enters
	 * the bounding box. If they are, overlay functionality is disabled
	 * until the overlays are closed by a button or the mouse leaves
	 * the bounding box. If only the nav overlay is visible, this
	 * function does nothing.
	 *
	 * @method _checkOverlaysOnMouseenter
	 * @protected
	 */
	_checkOverlaysOnMouseenter: function()
	{
		var navType			= this.get('navType'),
			navOverlay		= this.get('navOverlay'),
			nav 			= this._getNav(),
			overlayVisible 	= false;

		if(this.thumbs && navType != 'thumbs' && this.thumbs.slideshowOverlay._visible) {
			overlayVisible = true;
			this.thumbs.slideshowOverlay.disable();
		}
		else if(this.caption && this.caption.slideshowOverlay._visible) {
			overlayVisible = true;
			this.caption.slideshowOverlay.disable();
		}
		else if(this.social && this.social.slideshowOverlay._visible) {
			overlayVisible = true;
			this.social.slideshowOverlay.disable();
		}

		if(nav && overlayVisible && navOverlay) {
			nav.slideshowOverlay.disable();
		}
	},

	/**
	 * Checks whether a nav button is set or not.
	 *
	 * @method _hasNavButton
	 * @protected
	 * @param button {String} The button to look for.
	 * @returns Boolean
	 */
	_hasNavButton: function(button)
	{
		var navType = this.get('navType');

		if(navType == 'buttons' || navType == 'thumbs' || navType == 'custom') {
			if(Y.Array.indexOf(this.get('navButtons'), button) > -1) {
				return true;
			}
			else if(Y.Array.indexOf(this.get('navButtonsLeft'), button) > -1) {
				return true;
			}
			else if(Y.Array.indexOf(this.get('navButtonsRight'), button) > -1) {
				return true;
			}
			else {
				return false;
			}
		}
		else {
			return false;
		}
	},

	/**
	 * @method _removeNavButton
	 * @param button {String} The name of the button to remove.
	 * @protected
	 */
	_removeNavButton: function(button)
	{
		var buttons 			= this.get('navButtons'),
			buttonsLeft 		= this.get('navButtonsLeft'),
			buttonsRight 		= this.get('navButtonsRight'),
			vtTopNavButtons 	= this.get('verticalThumbsTopNavButtons'),
			vtBottomNavButtons 	= this.get('verticalThumbsBottomNavButtons');

		if(Y.Array.indexOf(buttons, button) > -1) {
			buttons.splice(Y.Array.indexOf(buttons, button), 1);
		}
		if(Y.Array.indexOf(buttonsLeft, button) > -1) {
			buttonsLeft.splice(Y.Array.indexOf(buttonsLeft, button), 1);
		}
		if(Y.Array.indexOf(buttonsRight, button) > -1) {
			buttonsRight.splice(Y.Array.indexOf(buttonsRight, button), 1);
		}
		if(Y.Array.indexOf(vtTopNavButtons, button) > -1) {
			vtTopNavButtons.splice(Y.Array.indexOf(vtTopNavButtons, button), 1);
		}
		if(Y.Array.indexOf(vtBottomNavButtons, button) > -1) {
			vtBottomNavButtons.splice(Y.Array.indexOf(vtBottomNavButtons, button), 1);
		}

		this._set('navButtons', buttons);
		this._set('navButtonsLeft', buttonsLeft);
		this._set('navButtonsRight', buttonsRight);
		this._set('verticalThumbsTopNavButtons', vtTopNavButtons);
		this._set('verticalThumbsBottomNavButtons', vtBottomNavButtons);
	}

}, {

	/**
	 * Custom CSS class name for the widget.
	 *
	 * @property CSS_PREFIX
	 * @type String
	 * @protected
	 * @static
	 */
	CSS_PREFIX: 'fl-slideshow',

	/**
	 * Static property used to define the default attribute configuration of
	 * the Widget.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

		/**
		 * What should happen when the main image is clicked.
		 * Options are none, gallery and url. If url is chosen,
		 * clickActionUrl must be set.
		 *
		 * @attribute clickAction
		 * @type String
		 * @default none
		 */
		clickAction: {
			value: 'none'
		},

		/**
		 * The redirect url to use when clickAction is set to url.
		 *
		 * @attribute clickActionUrl
		 * @type String
		 * @default none
		 */
		clickActionUrl: {
			value: ''
		},

		/**
		 * Whether to crop the main image.
		 *
		 * @attribute crop
		 * @type Boolean
		 * @default false
		 */
		crop: {
			value: false
		},

		/**
		 * Whether to only crop horizontal images or not.
		 *
		 * @attribute cropHorizontalsOnly
		 * @type Boolean
		 * @default false
		 */
		cropHorizontalsOnly: {
			value: false
		},

		/**
		 * Whether to always use the loading image between images
		 * or to only use it between albums.
		 *
		 * @attribute loadingImageAlwaysEnabled
		 * @type Boolean
		 * @default true
		 */
		loadingImageAlwaysEnabled: {
			value: true
		},

		/**
		 * The x and y position of the main image
		 * within the bounding box.
		 *
		 * @attribute position
		 * @type String
		 * @default center center
		 */
		position: {
			value: 'center center'
		},

		/**
		 * Whether to right click protect the main image.
		 *
		 * @attribute protect
		 * @type Boolean
		 * @default true
		 */
		protect: {
			value: true
		},

		/**
		 * Whether to resize the main image past
		 * its original width and height.
		 *
		 * @attribute upsize
		 * @type Boolean
		 * @default true
		 */
        upsize: {
			value: true
		},

		/**
		 * The type of transition to use. Possible values are
		 * none, fade, slideHorizontal and slideVertical. The
		 * value can also be a common seperated string of transitions
		 * that will be randomly chosen for each image.
		 *
		 * @attribute transition
		 * @type String
		 * @default fade
		 */
		transition: {
			value: 'fade'
		},

		/**
		 * The duration of the transition, measured in seconds.
		 *
		 * @attribute transitionDuration
		 * @type Number
		 * @default 1
		 */
		transitionDuration: {
			value: 1
		},

		/**
		 * The type of transition easing to use.
		 *
		 * @attribute transitionEasing
		 * @type String
		 * @default ease-out
		 */
		transitionEasing: {
			value: 'ease-out'
		},

		/**
		 * The amount of zoom to use for the Ken Burns effect.
		 *
		 * @attribute kenBurnsZoom
		 * @type Number
		 * @default 1.2
		 */
		kenBurnsZoom: {
            value: 1.2
		},

		/**
		 * The type of navigation to use. Possible values are
		 * buttons, thumbs, custon and none.
		 *
		 * @attribute navType
		 * @type String
		 * @default none
		 */
		navType: {
			value: 'none'
		},

		/**
		 * The position of the main nav. Possible values are top and bottom.
		 *
		 * @attribute navPosition
		 * @type String
		 * @default bottom
		 */
		navPosition: {
			value: 'bottom'
		},

		/**
		 * Whether to overlay the nav on top of the main image.
		 *
		 * @attribute navOverlay
		 * @type Boolean
		 * @default false
		 */
		navOverlay: {
			value: false
		},

		/**
		 * An array of button names used to render the main nav's buttons.
		 *
		 * @attribute navButtons
		 * @type Array
		 * @default []
		 */
		navButtons: {
			value: []
		},

		/**
		 * An array of button names used to render the main nav's left buttons.
		 *
		 * @attribute navButtonsLeft
		 * @type Array
		 * @default []
		 */
		navButtonsLeft: {
			value: []
		},

		/**
		 * An array of button names used to render the main nav's right buttons.
		 *
		 * @attribute navButtonsRight
		 * @type Array
		 * @default []
		 */
		navButtonsRight: {
			value: []
		},

		/**
		 * Whether to hide the overlays when the mouse moves or not.
		 *
		 * @attribute overlayHideOnMousemove
		 * @type String
		 * @default mouseover
		 */
		overlayHideOnMousemove: {
			value: true
		},

		/**
		 * How long to wait before hiding the overlays.
		 * Measured in milliseconds.
		 *
		 * @attribute overlayHideDelay
		 * @type Number
		 * @default false
		 */
		overlayHideDelay: {
			value: 3000
		},

		/**
		 * Whether to use the image nav or not. If true, a prev
		 * and next button will be overlaid on the main image.
		 *
		 * @attribute imageNavEnabled
		 * @type Boolean
		 * @default false
		 */
		imageNavEnabled: {
			value: false
		},

		/**
		 * Whether to use the mouse nav or not. If true, the cursor
		 * will turn into a prev or next button when over the slideshow.
		 *
		 * @attribute mouseNavEnabled
		 * @type Boolean
		 * @default false
		 */
		mouseNavEnabled: {
			value: false
		},

		/**
		 * Whether to hide the thumbs when clicking on a thumbnail
		 * image or not. Thumbs always hide navType is set to buttons.
		 *
		 * @attribute thumbsHideOnClick
		 * @type Boolean
		 * @default false
		 */
		thumbsHideOnClick: {
			value: true
		},

		/**
		 * The horizontal spacing between thumbs.
		 *
		 * @attribute thumbsHorizontalSpacing
		 * @type Number
		 * @default 15
		 */
		thumbsHorizontalSpacing: {
			value: 15
		},

		/**
		 * The vertical spacing between thumbs.
		 *
		 * @attribute thumbsVerticalSpacing
		 * @type Number
		 * @default 15
		 */
		thumbsVerticalSpacing: {
			value: 15
		},

		/**
		 * Whether to space the thumbs evenly within a page.
		 *
		 * @attribute thumbsSpaceEvenly
		 * @type Boolean
		 * @default true
		 */
		thumbsSpaceEvenly: {
			value: true
		},

		/**
		 * Whether to center single pages of thumbs.
		 *
		 * @attribute thumbsCenterSinglePage
		 * @type Boolean
		 * @default false
		 */
		thumbsCenterSinglePage: {
			value: true
		},

		/**
		 * Whether to pause the slideshow when a thumb is clicked.
		 *
		 * @attribute thumbsPauseOnClick
		 * @type Boolean
		 * @default false
		 */
		thumbsPauseOnClick: {
			value: false
		},

		/**
		 * The type of transition to use between pages of thumbs.
		 *
		 * @attribute thumbsTransition
		 * @type String
		 * @default slideHorizontal
		 */
		thumbsTransition: {
			value: 'slideHorizontal'
		},

		/**
		 * The duration of the transition between pages of thumbs.
		 *
		 * @attribute thumbsTransitionDuration
		 * @type Number
		 * @default 0.8
		 */
		thumbsTransitionDuration: {
			value: 0.8
		},

		/**
		 * The type of transition easing to use between pages of thumbs.
		 *
		 * @attribute thumbsTransitionEasing
		 * @type String
		 * @default ease-out
		 */
		thumbsTransitionEasing: {
			value: 'ease-out'
		},

		/**
		 * Whether to crop the thumbnails.
		 *
		 * @attribute thumbsImageCrop
		 * @type Boolean
		 * @default true
		 */
		thumbsImageCrop: {
			value: true
		},

		/**
		 * The width of each thumbnail.
		 *
		 * @attribute thumbsImageWidth
		 * @type Number
		 * @default 50
		 */
		thumbsImageWidth: {
			value: 50
		},

		/**
		 * The height of each thumbnail.
		 *
		 * @attribute thumbsImageHeight
		 * @type Number
		 * @default 50
		 */
		thumbsImageHeight: {
			value: 50
		},

		/**
		 * The text to use for the "read less" toggle link.
		 *
		 * @attribute captionLessLinkText
		 * @type String
		 * @default Read Less
		 */
		captionLessLinkText: {
			value: 'Read Less'
		},

		/**
		 * The text to use for the "read more" toggle link.
		 *
		 * @attribute captionMoreLinkText
		 * @type String
		 * @default Read More
		 */
		captionMoreLinkText: {
			value: 'Read More'
		},

		/**
		 * The length of the caption to show. If greater than -1,
		 * the text will be truncated and a read more link will
		 * be displayed. If set to -1, the entire caption will be shown.
		 *
		 * @attribute captionTextLength
		 * @type Number
		 * @default 200
		 */
		captionTextLength: {
			value: 200
		},

		/**
		 * Whether to strip out HTML tags in the caption
		 * text or not.
		 *
		 * @attribute captionStripTags
		 * @type Boolean
		 * @default false
		 */
		captionStripTags: {
			value: false
		},

		/**
		 * Whether to use the vertical thumbs or not.
		 *
		 * @attribute verticalThumbsEnabled
		 * @type Boolean
		 * @default false
		 */
		verticalThumbsEnabled: {
			value: false
		},

		/**
		 * Position of the vertical thumbs. Possible values
		 * are either left or right.
		 *
		 * @attribute verticalThumbsPosition
		 * @type String
		 * @default left
		 */
		verticalThumbsPosition: {
			value: 'left'
		},

		/**
		 * Whether to overlay the vertical thumbs
		 * on the main image or not.
		 *
		 * @attribute verticalThumbsOverlay
		 * @type Boolean
		 * @default false
		 */
		verticalThumbsOverlay: {
			value: false
		},

		/**
		 * The number of columns for the vertical thumbs.
		 *
		 * @attribute verticalThumbsColumns
		 * @type Number
		 * @default 1
		 */
		verticalThumbsColumns: {
			value: 1
		},

		/**
		 * Whether to use the vertical thumbs top nav or not.
		 *
		 * @attribute verticalThumbsTopNavEnabled
		 * @type Boolean
		 * @default false
		 */
		verticalThumbsTopNavEnabled: {
			value: false
		},

		/**
		 * An array of button names used to render
		 * the vertical thumbs top nav buttons.
		 *
		 * @attribute verticalThumbsTopNavButtons
		 * @type Array
		 * @default prevPage, nextPage
		 */
		verticalThumbsTopNavButtons: {
			value: ['prevPage', 'nextPage']
		},

		/**
		 * Whether to use the vertical thumbs bottom nav or not.
		 *
		 * @attribute verticalThumbsBottomNavEnabled
		 * @type Boolean
		 * @default false
		 */
		verticalThumbsBottomNavEnabled: {
			value: true
		},

		/**
		 * An array of button names used to render
		 * the vertical thumbs top nav buttons.
		 *
		 * @attribute verticalThumbsBottomNavButtons
		 * @type Array
		 * @default prevPage, nextPage
		 */
		verticalThumbsBottomNavButtons: {
			value: ['prevPage', 'nextPage']
		},

		/**
		 * The horizontal spacing between vertical thumbs.
		 *
		 * @attribute verticalThumbsHorizontalSpacing
		 * @type Number
		 * @default 15
		 */
		verticalThumbsHorizontalSpacing: {
			value: 15
		},

		/**
		 * The vertical spacing between vertical thumbs.
		 *
		 * @attribute verticalThumbsVerticalSpacing
		 * @type Number
		 * @default 15
		 */
		verticalThumbsVerticalSpacing: {
			value: 15
		},

		/**
		 * Whether to space the vertical thumbs evenly within a page.
		 *
		 * @attribute verticalThumbsSpaceEvenly
		 * @type Boolean
		 * @default false
		 */
		verticalThumbsSpaceEvenly: {
			value: false
		},

		/**
		 * Whether to pause the slideshow when a vertical thumb is clicked.
		 *
		 * @attribute verticalThumbsPauseOnClick
		 * @type Boolean
		 * @default false
		 */
		verticalThumbsPauseOnClick: {
			value: false
		},

		/**
		 * Whether to crop the vertical thumbs or not.
		 *
		 * @attribute verticalThumbsImageCrop
		 * @type Boolean
		 * @default true
		 */
		verticalThumbsImageCrop: {
			value: true
		},

		/**
		 * The width of each vertical thumbnail.
		 *
		 * @attribute verticalThumbsImageWidth
		 * @type Number
		 * @default 75
		 */
		verticalThumbsImageWidth: {
			value: 75
		},

		/**
		 * The height of each vertical thumbnail.
		 *
		 * @attribute verticalThumbsImageHeight
		 * @type Number
		 * @default 75
		 */
		verticalThumbsImageHeight: {
			value: 75
		},

		/**
		 * The type of transition to use between pages of vertical thumbs.
		 *
		 * @attribute verticalThumbsTransition
		 * @type String
		 * @default slideVertical
		 */
		verticalThumbsTransition: {
			value: 'slideVertical'
		},

		/**
		 * The duration of the transition between pages of vertical thumbs.
		 *
		 * @attribute verticalThumbsTransitionDuration
		 * @type Number
		 * @default 0.8
		 */
		verticalThumbsTransitionDuration: {
			value: 0.8
		},

		/**
		 * The type of transition easing to use between pages of vertical thumbs.
		 *
		 * @attribute verticalThumbsTransitionEasing
		 * @type String
		 * @default ease-out
		 */
		verticalThumbsTransitionEasing: {
			value: 'ease-out'
		},

		/**
		 * Whether to use the Facebook like button or not.
		 *
		 * @attribute likeButtonEnabled
		 * @type Boolean
		 * @default true
		 */
		likeButtonEnabled: {
			value: true
		},

		/**
		 * Whether to use the Pinterest button or not.
		 *
		 * @attribute pinterestButtonEnabled
		 * @type Boolean
		 * @default true
		 */
		pinterestButtonEnabled: {
			value: true
		},

		/**
		 * Whether to use the Tweet button or not.
		 *
		 * @attribute tweetButtonEnabled
		 * @type Boolean
		 * @default true
		 */
		tweetButtonEnabled: {
			value: true
		},

		/**
		 * Whether to use touch gestures, when available,
		 * to transition between images or not.
		 *
		 * @attribute touchSupport
		 * @type Boolean
		 * @default true
		 */
		touchSupport: {
			value: true
		},

		/**
		 * Whether to fallback to link tags or use button tags.
		 *
		 * @attribute fallback
		 * @type Boolean
		 * @default false
		 */
		fallback: {
			value: false
		},
	}
});


}, '2.0.0' ,{requires:['anim', 'event-mouseenter', 'plugin', 'transition', 'fl-event-move', 'fl-slideshow-css', 'fl-slideshow-base', 'fl-utils', 'sm-fonticon']});


YUI.add('fl-slideshow-album-loader', function(Y) {

/**
 * @module fl-slideshow-album-loader
 */

/**
 * Loads slideshow albums using a provided source object.
 *
 * @namespace FL
 * @class SlideshowAlbumLoader
 * @constructor
 * @param config {Object} Configuration object
 * @extends Base
 */
Y.namespace('FL').SlideshowAlbumLoader = Y.Base.create('fl-slideshow-album-loader', Y.Base, [], {

	/**
	* The source object used for loading.
	*
	* @property _source
	* @type Object
	* @default null
	* @protected
	*/
	_source: null,

	/**
	 * Loads slideshow album data using the provided source object.
	 *
	 * @method load
	 * @param source {Object} The source object to use for loading.
	 */
	load: function(source)
	{
		this._source = source;

		/**
		 * Fires before a new load request is made.
		 *
		 * @event start
		 */
		this.fire('start');

		this[Y.FL.SlideshowAlbumLoader.TYPES[source.type]].call(this);
	},

	 /**
	 * Called when a source type completes loading
	 * and fires the complete event.
	 *
	 * @method _loadComplete
	 * @param o {Object} Passed to complete event subscribers.
	 * @protected
	 */
	_loadComplete: function(o)
	{
		o = this._randomize(o);

		/**
		 * Fires after a new load request is made.
		 *
		 * @event complete
		 */
		this.fire('complete', o);
	},

	/**
	 * Randomizes images in an album.
	 *
	 * @method _randomize
	 * @param album {Object} The album to randomize.
	 * @protected
	 */
	_randomize: function(o)
	{
		var i;

		if(this.get('randomize')) {
			o.albumInfo.images.sort(function() { return 0.5 - Math.random(); });

			for(i = 0; i < o.albumInfo.images.length; i++) {
				o.albumInfo.images[i].index = i;
			}
		}

		return o;
	},

	/**
	 * Loads slideshow album data from SmugMug.
	 *
	 * @method _loadSmugMug
	 * @protected
	 */
	_loadSmugMug: function()
	{
		var sm = new Y.FL.SmugMugAPI();

		sm.on('complete', this._loadSmugMugSuccess, this);
		sm.addParam('method', 'smugmug.images.get');
		sm.addParam('AlbumID', this._source.id);
		sm.addParam('AlbumKey', this._source.key);
		sm.addParam('Extras', 'Caption,Format,FileName');

		// Gallery password
		if(this._source.password) {
			sm.addParam('Password', this._source.password);
		}

		// Site-wide password
		if(this._source.sp) {
			sm.addParam('SitePassword', this._source.sp);
		}

		sm.request();
	},

	/**
	 * Processes slideshow album data loaded from SmugMug.
	 *
	 * @method _loadSmugMugSuccess
	 * @param e {Object} The custom event object passed to this function.
	 * @protected
	 */
	_loadSmugMugSuccess: function(e)
	{
		var images 		= e.Album.Images,
			album 		= {},
			proxy       = typeof this._source.proxy !== 'undefined' ? this._source.proxy : '',
			buyBase		= '',
			baseURL 	= '',
			ext 		= '',
			format		= '',
			i			= 0,
			temp		= null,
			iframe		= null;

		album.index = this._source.index;
		album.id = e.Album.id;
		album.key = e.Album.Key;
		album.link = e.Album.URL;
		album.title = this._source.title ? this._source.title : '';
		album.images = [];
		buyBase = album.link.replace('https://', '').split('/').shift();
		buyBase = 'https://' + buyBase + '/buy/' + e.Album.id + '_' + e.Album.Key + '/';

		for(i = 0; i < images.length; i++)
		{
			baseURL = proxy + e.Album.URL + '/' + images[i].id + '_' + images[i].Key;
			format = images[i].Format.toLowerCase();
			ext = format == 'mp4' ? '.jpg' : '.' + format;

			album.images[i] = {};
			album.images[i].index = i;
			album.images[i].sourceType = 'smugmug';
			album.images[i].albumId = e.Album.id;
			album.images[i].albumKey = e.Album.Key;
			album.images[i].id = images[i].id;
			album.images[i].key = images[i].Key;
			album.images[i].filename = images[i].FileName;
			album.images[i].format = format;
			album.images[i].caption = images[i].Caption || '';
			album.images[i].link = e.Album.URL + '#' + images[i].id + '_' + images[i].Key;
			album.images[i].tinyURL = baseURL + '-Ti' + ext;
			album.images[i].thumbURL = baseURL + '-Th' + ext;
			album.images[i].smallURL = baseURL + '-S' + ext;
			album.images[i].mediumURL = baseURL + '-M' + ext;
			album.images[i].largeURL = baseURL + '-L' + ext;
			album.images[i].xlargeURL = baseURL + '-XL' + ext;
			album.images[i].x2largeURL = baseURL + '-X2' + ext;
			album.images[i].x3largeURL = baseURL + '-X3' + ext;
			album.images[i].buyURL = buyBase + images[i].id + '_' + images[i].Key;
			album.images[i].iframe = '';

			if(album.images[i].caption.indexOf('iframe')) {
				temp = Y.Node.create('<div>'+ album.images[i].caption +'</div>');
				iframe = temp.one('iframe');

				if(iframe) {
					album.images[i].iframe = iframe.getAttribute('src');
					album.images[i].caption = album.images[i].caption.replace(/<iframe.*>.*<\/iframe>/gi, '');
				}
			}
		}

		this._loadComplete({ 'albumInfo': album });
	},

	/**
	 * Loads slideshow album data from an array of urls.
	 *
	 * NOTE: You must have a large URL.
	 *
	 * @method _loadUrls
	 * @protected
	 */
	_loadUrls: function()
	{
		var album 	= {},
			i		= 0;

		album.index = this._source.index;
		album.title = this._source.title ? this._source.title : '';
		album.images = [];

		for( ; i < this._source.urls.length; i++)
		{
			album.images[i] = {};
			album.images[i].index = i;
			album.images[i].sourceType = 'urls';
			album.images[i].filename = this._source.urls[i].largeURL.split('/').pop();
			album.images[i].format = '';
			album.images[i].caption = this._source.urls[i].caption || '';
			album.images[i].alt = this._source.urls[i].alt || '';
			album.images[i].link = this._source.urls[i].largeURL;
			album.images[i].thumbURL = this._source.urls[i].thumbURL || this._source.urls[i].largeURL;
			album.images[i].smallURL = this._source.urls[i].smallURL || this._source.urls[i].largeURL;
			album.images[i].mediumURL = this._source.urls[i].mediumURL || this._source.urls[i].largeURL;
			album.images[i].largeURL = this._source.urls[i].largeURL; // Must have a large URL
			album.images[i].xlargeURL = this._source.urls[i].xlargeURL || this._source.urls[i].largeURL;
			album.images[i].x2largeURL = this._source.urls[i].x2largeURL || this._source.urls[i].largeURL;
			album.images[i].x3largeURL = this._source.urls[i].x3largeURL || this._source.urls[i].largeURL;
			album.images[i].buyURL = this._source.urls[i].buyURL || '';
			album.images[i].iframe = this._source.urls[i].iframe || '';
		}

		this._loadComplete({ 'albumInfo': album });
	}

}, {

	/**
	* Static property used to define the default attribute configuration of
	* the Widget.
	*
	* @property ATTRS
	* @type Object
	* @protected
	* @static
	*/
	ATTRS: {

		/**
		 * If true, the images will be randomized after loading.
		 *
		 * @attribute randomize
		 * @type Boolean
		 * @default false
		 */
		randomize: {
			value: false
		}
	},

	/**
	* The types of source data that can be loaded
	* and associated functions.
	*
	* @property TYPES
	* @type Object
	* @readOnly
	* @protected
	* @static
	*/
	TYPES: {
		'smugmug': '_loadSmugMug',
		'flickr': '_loadFlickr',
		'picasa': '_loadPicasa',
		'urls': '_loadUrls',
		'html': '_loadHtml'
	}
});


}, '2.0.0' ,{requires:['base', 'fl-smugmug-api']});


YUI.add('fl-slideshow-base', function(Y) {

/**
 * @module fl-slideshow-base
 */

/**
 * The base class that gets extended when creating new
 * slideshow widgets. Manages loading, playing, and resizing.
 * <p>
 * While SlideshowBase can be instantiated, it is only meant to
 * be extended and does not display any images.
 *
 * @namespace FL
 * @class SlideshowBase
 * @constructor
 * @param config {Object} Configuration object
 * @extends Widget
 */
Y.namespace('FL').SlideshowBase = Y.Base.create('fl-slideshow-base', Y.Widget, [Y.WidgetParent], {

	/**
	 * FL.SlideshowAlbumLoader instance used to load albums.
	 *
	 * @property _loader
	 * @type FL.SlideshowAlbumLoader
	 * @default null
	 * @protected
	 */
	_albumLoader: null,

	/**
	* An array of albums loaded from the source attribute.
	* Each album is an array of objects containing image info.
	*
	* @property albums
	* @type Array
	* @default []
	*/
	albums: [],

	/**
	 * Info for the active album.
	 *
	 * @property albumInfo
	 * @type Object
	 * @default null
	 */
	albumInfo: null,

	/**
	 * A number that represents the index of the active
	 * album in the albums array.
	 *
	 * @property albumIndex
	 * @type Number
	 * @default null
	 */
	albumIndex: null,

	/**
	 * Info for the active image.
	 *
	 * @property imageInfo
	 * @type Object
	 * @default null
	 */
	imageInfo: null,

	/**
	 * A number that represents the index of the active
	 * image in the albumInfo array.
	 *
	 * @property imageIndex
	 * @type Number
	 * @default null
	 */
	imageIndex: null,

	/**
	 * A number that represents the index of the last
	 * image that was loaded in the albumInfo array.
	 *
	 * @property lastImageIndex
	 * @type Number
	 * @default null
	 */
	lastImageIndex: null,

	/**
	 * Timer for the delay before resizing if one is set.
	 *
	 * @property _resizeTimer
	 * @type Object
	 * @default null
	 * @protected
	 */
	_resizeTimer: null,

	/**
	 * Flag for whether the slideshow is currently playing or not.
	 *
	 * @property playing
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_playing: false,

	/**
	 * Timer for the break in between images when
	 * the slideshow is playing.
	 *
	 * @property _playingTimer
	 * @type Object
	 * @default null
	 * @protected
	 */
	_playingTimer: null,

	/**
	 * If set, the slideshow will only auto start when
	 * this event is fired.
	 *
	 * @property _playingTimerEvent
	 * @type Object
	 * @default null
	 * @protected
	 */
	_playingTimerEvent: null,

	/**
	 * An instance of FL.Spinner that is shown and hidden
	 * using _showLoadingImage and _hideLoadingImage when
	 * a loading activity occurs.
	 *
	 * @property _loadingImage
	 * @type FL.Spinner
	 * @default null
	 * @protected
	 */
	_loadingImage: null,

	/**
	 * An div node that wraps the loading image.
	 *
	 * @property _loadingImageWrap
	 * @type Node
	 * @default null
	 * @protected
	 */
	_loadingImageWrap: null,

	/**
	 * Whether the loading image is visible or not.
	 *
	 * @property _loadingImageVisible
	 * @type Boolean
	 * @default false
	 * @protected
	 */
	_loadingImageVisible: false,

	/**
	 * A timer to delay the display of the loading image.
	 *
	 * @property _loadingImageTimer
	 * @type Object
	 * @default null
	 * @protected
	 */
	_loadingImageTimer: null,

	/**
	 * The container to insert the loading image into. If
	 * no container is set, the loading image will be inserted
	 * into the widget's bounding box.
	 *
	 * @property _loadingImageContainer
	 * @type Object
	 * @default null
	 * @protected
	 */
	_loadingImageContainer: null,

	/**
	 * The intial height of the slideshow. Used to resize
	 * back to the starting height when exiting stretchy.
	 *
	 * @property _initialHeight
	 * @type Number
	 * @default null
	 * @protected
	 */
	_initialHeight: null,

	/**
	 * The intial width of the slideshow. Used to resize
	 * back to the starting width when exiting stretchy.
	 *
	 * @property _initialWidth
	 * @type Number
	 * @default null
	 * @protected
	 */
	_initialWidth: null,

	/**
	 * @method initializer
	 * @protected
	 */
	initializer: function()
	{
		// Loader
		this._albumLoader = new Y.FL.SlideshowAlbumLoader({
			randomize: this.get('randomize')
		});
	},

	/**
	 * @method renderUI
	 * @protected
	 */
	renderUI: function()
	{
		this._renderLoadingImage();
	},

	/**
	 * @method bindUI
	 * @protected
	 */
	bindUI: function()
	{
		// Album load complete
		this._albumLoader.on('complete', this._loadAlbumComplete, this);

		// Resize Events
		Y.one(window).on('fl-slideshow-base|resize', this._delayResize, this);
		Y.one(window).on('fl-slideshow-base|orientationchange', this._delayResize, this);

		// Key Events
		Y.Node.one('body').on('keydown', Y.bind(this._onKey, this));
	},

	/**
	 * @method syncUI
	 * @protected
	 */
	syncUI: function()
	{
		this.get('boundingBox').addClass('fl-slideshow-' + this.get('color'));

		this.resize();

		if(this.get('loadOnRender')) {
			this.loadAlbum(this.get('defaultAlbum'), this.get('defaultImage'));
		}
	},

	/**
	 * Add album data to the source object.
	 *
	 * @method addAlbum
	 * @protected
	 */
	addAlbum: function(data)
	{
		var source = this.get('source'),
            i      = source.length;

		source[i] = data;
		source[i].index = i;

		this.set('source', source);
	},

	/**
	 * Loads an album from the source array with the provided albumIndex.
	 * If no albumIndex is provided, the first album in the array will be loaded.
	 * An image to load can also be specified using imageIndex.
	 *
	 * @method loadAlbum
	 * @param albumIndex {Number} The album index to load from the source array.
	 * @param imageIndex {Number} The image index to load from the album array.
	 */
	loadAlbum: function(albumIndex, imageIndex)
	{
		var source 			= this.get('source'),
			loadImageIndex 	= typeof imageIndex == 'undefined' ? 0 : imageIndex;

		// Reset internal image indexes.
		this.imageIndex = null;
		this.lastImageIndex = null;

		/**
		 * Fires before a new album request is made.
		 *
		 * @event albumLoadStart
		 */
		this.fire('albumLoadStart');

		// Load an image after the album.
		this.once('albumLoadComplete', Y.bind(this.loadImage, this, loadImageIndex));

		// Load data passed from another slideshow instance.
		if(source[albumIndex] && source[albumIndex].type == 'album-data') {
			this.albums[albumIndex] = source[albumIndex].data;
			this._loadAlbumComplete({albumInfo: this.albums[albumIndex]});
		}
		// Load the album from the albums array.
		else if(source[albumIndex] && this.albums[albumIndex]) {
			this._loadAlbumComplete({albumInfo: this.albums[albumIndex]});
		}
		// Load the album using the album loader.
		else {
			this._albumLoader.load(source[albumIndex] || source[0]);
		}
	},

	/**
	 * Processes the loaded album and fires the albumLoadComplete event.
	 *
	 * @method _loadAlbumComplete
	 * @param o {Object} The custom event object passed to this method.
	 * @protected
	 */
	_loadAlbumComplete: function(o)
	{
		this.albums[o.albumInfo.index] = o.albumInfo;
		this.albumInfo = o.albumInfo;
		this.albumIndex = o.albumInfo.index;

		/**
		 * Fires after a new album request is made.
		 *
		 * @event albumLoadComplete
		 */
		this.fire('albumLoadComplete');

		// Auto Play
		if(this.get('autoPlay')) {
			this._playingTimerStart();
			this.fire('played');
			this._playing = true;
		}
	},

	/**
	 * Sets the active image index and fires the imageLoadComplete event.
	 *
	 * @method loadImage
	 * @param index {Number} The image index to load.
	 */
	loadImage: function(index)
	{
		if(this._playing) {
			this._playingTimerStart();
		}

		index = index < 0 ? this.albumInfo.images.length - 1 : index;
		index = index >= this.albumInfo.images.length ? 0 : index;

		this.lastImageIndex = this.imageIndex;
		this.imageIndex = index;
		this.imageInfo = this.albumInfo.images[index];

		/**
		 * Fires after a new image index is set.
		 *
		 * @event imageLoadComplete
		 */
		this.fire('imageLoadComplete', { 'imageInfo': this.imageInfo });
	},

	/**
	 * Loads the previous image.
	 *
	 * @method prevImage
	 */
	prevImage: function()
	{
		if(this.get('pauseOnNextOrPrev')) {
			this.pause();
		}

		this.loadImage(this.imageIndex - 1);

		/**
		 * Fires when the previous image is loaded.
		 *
		 * @event prevImage
		 */
		this.fire('prevImage');
	},

	/**
	 * Loads the next image.
	 *
	 * @method nextImage
	 */
	nextImage: function()
	{
		if(this.get('pauseOnNextOrPrev')) {
			this.pause();
		}

		this.loadImage(this.imageIndex + 1);

		/**
		 * Fires when the next image is loaded.
		 *
		 * @event nextImage
		 */
		this.fire('nextImage');
	},

	/**
	 * Keyboard navigation for the next and prev images.
	 *
	 * @method _onKey
	 * @protected
	 */
	_onKey: function(e)
	{
		switch(e.keyCode) {

			case 37:
			this.prevImage();
			break;

			case 39:
			this.nextImage();
			break;
		}
	},

	/**
	 * Resizes the slideshow using either the
	 * stretchy or standard functions.
	 *
	 * @method resize
	 */
	resize: function()
	{
		var stretchy 		= this.get('stretchy'),
			stretchyType 	= this.get('stretchyType'),
			width 			= parseInt(Y.one('body').get('winWidth'), 10),
			threshold		= this.get('responsiveThreshold');

        // Stretchy resize to the window only if the parent width is greater
        // than the responsive threshold and stretchyType is set to window.
		if(width > threshold && stretchy && stretchyType == 'window') {
			this._stretchyWindowResize();
		}

		// Ratio resize if the parent width is less than the responsive
		// threshold or if stretchyType is set to ratio.
		else if((width <= threshold) || (stretchy && stretchyType == 'ratio')) {
			this._stretchyRatioResize();
		}

		// Do a standard resize based on the height and
		// width passed to the constructor function.
		else {
			this._standardResize();
		}

		/**
		 * Fires when the slideshow is resized.
		 *
		 * @event resize
		 */
		this.fire('resize');
	},

	/**
	 * @method _standardResize
	 * @protected
	 */
	_standardResize: function()
	{
		var stretchy 		= this.get('stretchy'),
		    stretchyType 	= this.get('stretchyType'),
		    bb		        = this.get('boundingBox'),
			parent	        = bb.get('parentNode'),
			parentHeight 	= parseInt(parent.getComputedStyle('height'), 10),
			parentWidth 	= parseInt(parent.getComputedStyle('width'), 10),
			height 	        = this.get('height'),
			width 	        = this.get('width');

        // Window resize if we are in fullscreen.
        if(bb.hasClass('fl-fullscreen-active')) {
			this._stretchyWindowResize();
			return;
		}

		// Resize to the width and height of the parent.
		else if(stretchy && stretchyType == 'contain') {
			bb.setStyle('height', parentHeight + 'px');
    		bb.setStyle('width', parentWidth + 'px');
		}

		// Ratio resize if we don't have a height defined.
		else if(!Y.Lang.isNumber(height)) {
			this._stretchyRatioResize();
			return;
		}

		// Resize to the defined width and height.
		else {

    		bb.setStyle('height', height + 'px');

    		if(width) {
    			bb.setStyle('width', width + 'px');
    		}
    		else {
        		bb.setStyle('width', parentWidth + 'px');
    		}
		}
	},

	/**
	 * Resizes to the height of the window, compensating
	 * for any padding.
	 *
	 * @method _stretchyWindowResize
	 * @protected
	 */
	_stretchyWindowResize: function()
	{
		var bb				= this.get('boundingBox'),
			verticalSpace	= this.get('stretchyVerticalSpace'),
			paddingTop 		= parseInt(bb.getStyle('paddingTop'), 10),
			paddingBottom 	= parseInt(bb.getStyle('paddingBottom'), 10),
			height 			= parseInt(Y.one('body').get('winHeight'), 10),
			width			= '';

		// Set the vertical space to 0 and width to the
		// window's width if we are in fullscreen mode.
		if(bb.hasClass('fl-fullscreen-active')) {
			verticalSpace = 0;
			width = parseInt(Y.one('body').get('winWidth'), 10) + 'px';
		}

		height =  (height - paddingTop - paddingBottom - verticalSpace) + 'px';

		bb.setStyle('height', height);
		bb.setStyle('width', width);
	},

	/**
	 * Resizes the height by multiplying the width and stretchyRatio value.
	 *
	 * @method _stretchyRatioResize
	 * @protected
	 */
	_stretchyRatioResize: function()
	{
		var bb				= this.get('boundingBox'),
			parent			= bb.get('parentNode'),
			verticalSpace	= 0,
			stretchyRatio	= this.get('stretchyRatio'),
			paddingTop 		= parseInt(bb.getStyle('paddingTop'), 10),
			paddingBottom 	= parseInt(bb.getStyle('paddingBottom'), 10),
			computedWidth 	= parseInt(parent.getComputedStyle('width'), 10),
			winHeight		= parseInt(Y.one('body').get('winHeight'), 10),
			winWidth		= parseInt(Y.one('body').get('winWidth'), 10),
			height 			= computedWidth * stretchyRatio,
			width			= '';

		// Use the window's height and width if we are in fullscreen mode.
		if(bb.hasClass('fl-fullscreen-active')) {
			height = winHeight;
			width = winWidth;
		}

		height = (height - paddingTop - paddingBottom - verticalSpace) + 'px';

		bb.setStyle('height', height);
		bb.setStyle('width', width);
	},

	/**
	 * Resizes the slideshow after the resize timer completes.
	 *
	 * @method _delayResize
	 * @protected
	 */
	_delayResize: function()
	{
		if(this._resizeTimer) {
			this._resizeTimer.cancel();
		}

		this._resizeTimer = Y.later(300, this, this.resize);
	},

	/**
	 * Starts a new playing timer and fires the played event.
	 *
	 * @method play
	 */
	play: function()
	{
		this._playingTimer = Y.later(this.get('speed'), this, this._playingTimerComplete);

		/**
		 * Fires when the playing timer starts.
		 *
		 * @event played
		 */
		this.fire('played');
		this._playing = true;
	},

	/**
	 * Cancels the current playing timer and fires the paused event.
	 *
	 * @method pause
	 */
	pause: function()
	{
		this._playingTimerCancel();

		/**
		 * Fires when the playing timer is canceled.
		 *
		 * @event paused
		 */
		this.fire('paused');
		this._playing = false;
	},

	/**
	 * A new playing timer will start when this event is fired.
	 *
	 * @method _setPlayingTimerEvent
	 * @param obj {Object} The event's host object.
	 * @param e {String} The event to fire on the host object.
	 * @protected
	 */
	_setPlayingTimerEvent: function(obj, e)
	{
		this._playingTimerEvent = {
			'obj': obj,
			'e': e
		};
	},

	/**
	 * Cancels the playing timer if it is running and starts a new one.
	 * The next image is loaded when the timer completes.
	 *
	 * @method _playingTimerStart
	 * @protected
	 */
	_playingTimerStart: function(e)
	{
		this._playingTimerCancel();

		if(!e && this._playingTimerEvent !== null) {
			this._playingTimerEvent.obj.once('fl-slideshow-base|' + this._playingTimerEvent.e, Y.bind(this._playingTimerStart, this));
		}
		else {
			this._playingTimer = Y.later(this.get('speed'), this, this._playingTimerComplete);
		}
	},

	/**
	 * Fires when the playing timer completes, starts a
	 * new timer and loads the next image.
	 *
	 * @method _playingTimerComplete
	 * @protected
	 */
	_playingTimerComplete: function()
	{
		this.loadImage(this.imageIndex + 1);

		/**
		 * Fires when the playing timer completes.
		 *
		 * @event albumLoadStart
		 */
		this.fire('playingTimerComplete');
	},

	/**
	 * Cancels the playing timer.
	 *
	 * @method _playingTimerCancel
	 * @protected
	 */
	_playingTimerCancel: function()
	{
		if(this._playingTimer) {
			this._playingTimer.cancel();
		}
		if(this._playingTimerEvent) {
			this._playingTimerEvent.obj.detach('fl-slideshow-base|' + this._playingTimerEvent.e);
		}
	},

	/**
	 * Creates the loading image.
	 *
	 * @method _renderLoadingImage
	 * @protected
	 */
	_renderLoadingImage: function()
	{
        var defaults = {
                lines: 11, // The number of lines to draw
                length: 6, // The length of each line
                width: 2, // The line thickness
                radius: 7, // The radius of the inner circle
                color: '', // #rbg or #rrggbb
                speed: 1, // Rounds per second
                trail: 60, // Afterglow percentage
                shadow: false // Whether to render a shadow
            },
            settings = Y.merge(defaults, this.get('loadingImageSettings'));

		if(this.get('loadingImageEnabled')) {

            // Loading image
            if(settings.color === '') {
                settings.color = this._colorToHex(Y.one('body').getStyle('color'));
            }

            this._loadingImage = new Y.FL.Spinner(settings);

            // Loading image wrap
            this._loadingImageWrap = Y.Node.create('<div class="fl-loading-image"></div>');

            this._loadingImageWrap.setStyles({
                position    : 'absolute',
                'z-index'   : '1000'
			});
		}
	},

	/**
	 * Inserts the loading image.
	 *
	 * @method _showLoadingImage
	 * @protected
	 */
	_showLoadingImage: function()
	{
		if(this._loadingImage && !this._loadingImageVisible) {

            this._loadingImageVisible = true;
            this._loadingImage.spin();
			this._loadingImageWrap.insert(this._loadingImage.el);

			if(this._loadingImageContainer !== null) {
                this._loadingImageContainer.insert(this._loadingImageWrap);
			}
			else {
                this.get('contentBox').insert(this._loadingImageWrap);
			}

			this._positionLoadingImage();
		}
	},

	/**
	 * Inserts the loading image div node after
	 * a timer completes.
	 *
	 * @method _showLoadingImageWithDelay
	 * @protected
	 */
	_showLoadingImageWithDelay: function()
	{
		if(this._loadingImage) {
			this._loadingImageTimer = Y.later(1000, this, this._showLoadingImage);
		}
	},

	/**
	 * Removes the loading image div node.
	 *
	 * @method _hideLoadingImage
	 * @protected
	 */
	_hideLoadingImage: function()
	{
		if(this._loadingImageTimer) {
			this._loadingImageTimer.cancel();
			this._loadingImageTimer = null;
		}
		if(this._loadingImage && this._loadingImageVisible) {
            this._loadingImageVisible = false;
			this._loadingImage.stop();
			this._loadingImageWrap.remove();
		}
	},

	/**
	 * Centers the loading image in the content box.
	 *
	 * @method _positionLoadingImage
	 * @protected
	 */
	_positionLoadingImage: function()
	{
		if(this._loadingImage && this._loadingImageVisible) {

            var wrap            = this._loadingImageWrap,
        		wrapHeight      = parseInt(wrap.getComputedStyle('height'), 10),
                wrapWidth       = parseInt(wrap.getComputedStyle('width'), 10),
        		parent          = wrap.get('parentNode'),
        		parentHeight    = parseInt(parent.getComputedStyle('height'), 10),
                parentWidth     = parseInt(parent.getComputedStyle('width'), 10),
        		left            = (parentWidth - wrapWidth)/2,
        		top             = (parentHeight - wrapHeight)/2;

			wrap.setStyles({
                left        : left + 'px',
                top         : top + 'px'
			});

			Y.one(this._loadingImage.el).setStyles({
                left        : '50%',
                top         : '50%'
			});
		}
	},

	/**
	 * Convert RGB color value to a hex value.
	 *
	 * @method _colorToHex
	 * @protected
	 */
	_colorToHex: function(color)
	{
        var digits, red, green, blue, rgb;

        if(color.substr(0, 1) === '#') {
            return color;
        }

        digits = /(.*?)rgb\((\d+), (\d+), (\d+)\)/.exec(color);

        if ( null === digits ) {
	        return '#000';
        }

        red = parseInt(digits[2], 10);
        green = parseInt(digits[3], 10);
        blue = parseInt(digits[4], 10);
        rgb = blue | (green << 8) | (red << 16);
        rgb = rgb.toString(16);

        if(rgb === '0') {
            rgb = '000';
        }

        return digits[1] + '#' + rgb;
    }

}, {

	/**
	 * Custom CSS class name for the widget.
	 *
	 * @property CSS_PREFIX
	 * @type String
	 * @protected
	 * @static
	 */
	CSS_PREFIX: 'fl-slideshow-base',

	/**
	 * Static property used to define the default attribute configuration of
	 * the Widget.
	 *
	 * @property ATTRS
	 * @type Object
	 * @protected
	 * @static
	 */
	ATTRS: {

		/**
		 * Used to create the color class that gets added to the bounding box
		 * when the widget is rendered. The color class is used to create new
		 * CSS color themes. The default CSS provided includes dark and light themes.
		 *
		 * @attribute color
		 * @type String
		 * @default dark
		 * @writeOnce
		 */
		color: {
			value: 'dark',
			writeOnce: true
		},

		/**
		 * An array of source objects used to load albums. Each object must have
		 * a type property and can have a title property as well.
		 * <p>
		 * In addition to those properties, each object has additional required
		 * properties specific to its type. The types currently supported are
		 * smugmug and urls with planned support for flickr and picasa.
		 * See the user guide for information on loading different types.
		 *
		 * @attribute source
		 * @type Array
		 * @default []
		 * @writeOnce
		 */
		source: {
			value: [],
			setter: function(source) {

				if(source.constructor == Object) {
					source = [source];
				}

				for(var i = 0; i < source.length; i++) {
					source[i].index = i;
				}

				return source;
			}
		},

		/**
		 * The default album index to load.
		 *
		 * @attribute defaultAlbum
		 * @type Number
		 * @default 0
		 */
		defaultAlbum: {
			value: 0
		},

		/**
		 * The default image index to load.
		 *
		 * @attribute defaultImage
		 * @type Number
		 * @default 0
		 */
		defaultImage: {
			value: 0
		},

		/**
		 * If true, the slideshow will be loaded after rendering.
		 *
		 * @attribute loadOnRender
		 * @type Boolean
		 * @default true
		 */
		loadOnRender: {
			value: true
		},

		/**
		 * If true, the slideshow will start playing after loading.
		 *
		 * @attribute autoPlay
		 * @type Boolean
		 * @default true
		 */
		autoPlay: {
			value: true
		},

		/**
		 * Whether to pause when the next or previous image is loaded
		 * using nextImage or prevImage. The slideshow will not be paused
		 * if the next or previous image is loaded using loadImage as is the
		 * case when the slideshow is playing.
		 *
		 * @attribute pauseOnNextOrPrev
		 * @type Boolean
		 * @default true
		 */
		pauseOnNextOrPrev: {
			value: true
		},

		/**
		 * If true, the images will be randomized after loading.
		 *
		 * @attribute randomize
		 * @type Boolean
		 * @default false
		 */
		randomize: {
			value: false
		},

		/**
		 * The time between images when playing, measured in milliseconds.
		 *
		 * @attribute speed
		 * @type Number
		 * @default 4000
		 */
		speed: {
			value: 4000
		},

		/**
		 * The minimum width of the parent node at which
		 * responsive features are enabled. Set to 0 to
		 * disable responsive features as they are enabled
		 * whether stretchy is set to true or not.
		 *
		 * @attribute responsiveThreshold
		 * @type Number
		 * @default 600
		 */
		responsiveThreshold: {
			value: 600
		},

		/**
		 * Whether stretchy resizing should be enabled.
		 *
		 * @attribute stretchy
		 * @type Boolean
		 * @default false
		 */
		stretchy: {
			value: false
		},

		/**
		 * The type of stretchy logic to use. Possible values are
		 * window and ratio. Both types resize the width of the
		 * slideshow to the width of its parent node. With window, the
		 * height of the slideshow is resized to the height of the window.
		 * With ratio, the height of the slideshow is resized based
		 * on the ratio set with stretchyRatio or the height of the window
		 * if the ratio height is greater than the window height.
		 *
		 * @attribute stretchyType
		 * @type String
		 * @default ratio
		 */
		stretchyType: {
			value: 'ratio'
		},

		/**
		 * The number of pixels to subtract from the height of
		 * the slideshow when stretchy is set to true.
		 *
		 * @attribute stretchyVerticalSpace
		 * @type Number
		 * @default 0
		 */
		stretchyVerticalSpace: {
			value: 0
		},

		/**
		 * Used to calculate the height of the slideshow when stretchyType
		 * is set to ratio.
		 *
		 * @attribute stretchyRatio
		 * @type Number
		 * @default 0.7
		 */
		stretchyRatio: {
			value: 0.7
		},

		/**
		 * Whether to use the loading image or not.
		 *
		 * @attribute loadingImageEnabled
		 * @type Boolean
		 * @default true
		 */
		loadingImageEnabled: {
			value: true
		},

		/**
		 * Property object for setting up the spin.js loading image.
		 * For a complete list of properties see:
		 * http://effinroot.eiremedia.netdna-cdn.com/repo/plugins/misc/spin.js/index.html
		 *
		 * @attribute loadingImageSettings
		 * @type Object
		 */
		loadingImageSettings: {
			value: {}
		}
	}
});


}, '2.0.0' ,{requires:['node', 'base', 'widget', 'widget-parent', 'widget-child', 'fl-slideshow-album-loader', 'fl-spinner']});


YUI.add('fl-smugmug-api', function(Y) {

/**
 * @module fl-smugmug-api
 */

/**
 * SmugMug API wrapper.
 *
 * NOTE: Only anonymous logins are currently supported.
 *
 * @namespace FL
 * @class SmugMugAPI
 * @constructor
 * @param config {Object} Configuration object
 * @extends Base
 */
Y.namespace('FL').SmugMugAPI = Y.Base.create('fl-smugmug-api', Y.Base, [], {

	/**
	 * ID for the current session.
	 *
	 * @property _sessionID
	 * @type String
	 * @default null
	 * @protected
	 */
	_sessionID: null,

	/**
	 * URL with parameters for the next API request.
	 * Reset after each request.
	 *
	 * @property _requestURL
	 * @type String
	 * @default null
	 * @protected
	 */
	_requestURL: null,

	/**
	 * Lifecycle method. Initializes the request url.
     *
     * @method initializer
     * @protected
     */
	initializer: function()
	{
		this._resetRequestURL();
	},

	/**
	 * Adds a key/value pair to the request url.
	 *
	 * @method addParam
	 * @param key {String} The name of the parameter (example: key=val).
	 * @param val {String} The value of the parameter (example: key=val).
	 */
	addParam: function(key, val)
	{
		this._requestURL = this._requestURL + '&' + key + '=' + val;
	},

	/**
	 * Requests an anonymous login session.
	 *
	 * @method loginAnon
	 */
	loginAnon: function()
	{
	    this.addParam('method', 'smugmug.login.anonymously');
	    this.once('complete', this._loginAnonComplete);
	    this.request();
	},

	/**
	 * Anonymous login success handler.
	 *
	 * @method _loginAnonComplete
	 * @param data {Object} A jsonp data object.
	 * @protected
	 */
	_loginAnonComplete: function(data)
	{
    	if(data.Login) {
    		this._sessionID = data.Login.Session.id;
    	}
	},

	/**
	 * Sends an API request using the request url.
	 *
	 * @method request
	 */
	request: function()
	{
		this.addParam('Callback', '{callback}');

	   	Y.jsonp(this._requestURL, {
		    on: {
		        success: this._requestComplete,
		        timeout: function(){} // TODO: Handle timeouts
		    },
		    context: this,
		    timeout: 60000,
		    args: []
		});
	},

	/**
	 * API request complete handler.
	 *
	 * @method _requestComplete
	 * @param data {Object} A jsonp data object.
	 * @protected
	 */
	_requestComplete: function(data)
	{
		this._resetRequestURL();

		/**
		 * Fires when a request is complete.
		 *
		 * @event complete
		 */
		this.fire('complete', data);
	},

	/**
	 * Clears all parameters on the request url except
	 * the API key and session ID.
	 *
	 * @method _resetRequestURL
	 * @protected
	 */
	_resetRequestURL: function()
	{
		this._requestURL = this.get('apiURL') + '?APIKey=' + this.get('apiKey');

		if(this._sessionID) {
	        this.addParam('SessionID', this._sessionID);
	    }
	}

}, {

	/**
	* Static property used to define the default attribute configuration of
	* the Widget.
	*
	* @property ATTRS
	* @type Object
	* @protected
	* @static
	*/
	ATTRS: {

        /**
		* SmugMug API url to use for requests.
		*
		* @attribute apiUrl
		* @type String
		* @default https://api.smugmug.com/services/api/json/1.3.0/
		*/
		apiURL: {
        	value: 'https://api.smugmug.com/services/api/json/1.3.0/'
        },

        /**
		* SmugMug API key.
		*
		* @attribute apiKey
		* @type String
		* @default 7w6kuU5Ee6KSgRRExf2KLgppdkez9JD2
		*/
		apiKey: {
        	value: '7w6kuU5Ee6KSgRRExf2KLgppdkez9JD2'
        }
	}
});


}, '2.0.0' ,{requires:['base', 'jsonp']});


YUI.add('fl-spinner', function(Y) {

(function(window,document,undefined){var width="width",length="length",radius="radius",lines="lines",trail="trail",color="color",opacity="opacity",speed="speed",shadow="shadow",style="style",height="height",left="left",top="top",px="px",childNodes="childNodes",firstChild="firstChild",parentNode="parentNode",position="position",relative="relative",absolute="absolute",animation="animation",transform="transform",Origin="Origin",Timeout="Timeout",coord="coord",black="#000",styleSheets=style+"Sheets",prefixes="webkit0Moz0ms0O".split(0),animations={},useCssAnimations;function eachPair(args,it){var end=~~((args[length]-1)/2);for(var i=1;i<=end;i++){it(args[i*2-1],args[i*2])}}function createEl(tag){var el=document.createElement(tag||"div");eachPair(arguments,function(prop,val){el[prop]=val});return el}function ins(parent,child1,child2){if(child2&&!child2[parentNode]){ins(parent,child2)}parent.insertBefore(child1,child2||null);return parent}ins(document.getElementsByTagName("head")[0],createEl(style));var sheet=document[styleSheets][document[styleSheets][length]-1];function addAnimation(to,end){var name=[opacity,end,~~(to*100)].join("-"),dest="{"+opacity+":"+to+"}",i;if(!animations[name]){for(i=0;i<prefixes[length];i++){try{sheet.insertRule("@"+(prefixes[i]&&"-"+prefixes[i].toLowerCase()+"-"||"")+"keyframes "+name+"{0%{"+opacity+":1}"+end+"%"+dest+"to"+dest+"}",sheet.cssRules[length])}catch(err){}}animations[name]=1}return name}function vendor(el,prop){var s=el[style],pp,i;if(s[prop]!==undefined){return prop}prop=prop.charAt(0).toUpperCase()+prop.slice(1);for(i=0;i<prefixes[length];i++){pp=prefixes[i]+prop;if(s[pp]!==undefined){return pp}}}function css(el){eachPair(arguments,function(n,val){el[style][vendor(el,n)||n]=val});return el}function defaults(obj){eachPair(arguments,function(prop,val){if(obj[prop]===undefined){obj[prop]=val}});return obj}var Spinner=function Spinner(o){this.opts=defaults(o||{},lines,12,trail,100,length,7,width,5,radius,10,color,black,opacity,1/4,speed,1)},proto=Spinner.prototype={spin:function(target){var self=this,el=self.el=self[lines](self.opts);if(target){ins(target,css(el,left,~~(target.offsetWidth/2)+px,top,~~(target.offsetHeight/2)+px),target[firstChild])}if(!useCssAnimations){var o=self.opts,i=0,f=20/o[speed],ostep=(1-o[opacity])/(f*o[trail]/100),astep=f/o[lines];(function anim(){i++;for(var s=o[lines];s;s--){var alpha=Math.max(1-(i+s*astep)%f*ostep,o[opacity]);self[opacity](el,o[lines]-s,alpha,o)}self[Timeout]=self.el&&window["set"+Timeout](anim,50)})()}return self},stop:function(){var self=this,el=self.el;window["clear"+Timeout](self[Timeout]);if(el&&el[parentNode]){el[parentNode].removeChild(el)}self.el=undefined;return self}};proto[lines]=function(o){var el=css(createEl(),position,relative),animationName=addAnimation(o[opacity],o[trail]),i=0,seg;function fill(color,shadow){return css(createEl(),position,absolute,width,(o[length]+o[width])+px,height,o[width]+px,"background",color,"boxShadow",shadow,transform+Origin,left,transform,"rotate("+~~(360/o[lines]*i)+"deg) translate("+o[radius]+px+",0)","borderRadius","100em")}for(;i<o[lines];i++){seg=css(createEl(),position,absolute,top,1+~(o[width]/2)+px,transform,"translate3d(0,0,0)",animation,animationName+" "+1/o[speed]+"s linear infinite "+(1/o[lines]/o[speed]*i-1/o[speed])+"s");if(o[shadow]){ins(seg,css(fill(black,"0 0 4px "+black),top,2+px))}ins(el,ins(seg,fill(o[color],"0 0 1px rgba(0,0,0,.1)")))}return el};proto[opacity]=function(el,i,val){el[childNodes][i][style][opacity]=val};var behavior="behavior",URL_VML="url(#default#VML)",tag="group0roundrect0fill0stroke".split(0);(function(){var s=css(createEl(tag[0]),behavior,URL_VML),i;if(!vendor(s,transform)&&s.adj){for(i=0;i<tag[length];i++){sheet.addRule(tag[i],behavior+":"+URL_VML)}proto[lines]=function(){var o=this.opts,r=o[length]+o[width],s=2*r;function grp(){return css(createEl(tag[0],coord+"size",s+" "+s,coord+Origin,-r+" "+-r),width,s,height,s)}var g=grp(),margin=~(o[length]+o[radius]+o[width])+px,i;function seg(i,dx,filter){ins(g,ins(css(grp(),"rotation",360/o[lines]*i+"deg",left,~~dx),ins(css(createEl(tag[1],"arcsize",1),width,r,height,o[width],left,o[radius],top,-o[width]/2,"filter",filter),createEl(tag[2],color,o[color],opacity,o[opacity]),createEl(tag[3],opacity,0))))}if(o[shadow]){for(i=1;i<=o[lines];i++){seg(i,-2,"progid:DXImage"+transform+".Microsoft.Blur(pixel"+radius+"=2,make"+shadow+"=1,"+shadow+opacity+"=.3)")}}for(i=1;i<=o[lines];i++){seg(i)}return ins(css(createEl(),"margin",margin+" 0 0 "+margin,position,relative),g)};proto[opacity]=function(el,i,val,o){o=o[shadow]&&o[lines]||0;el[firstChild][childNodes][i+o][firstChild][firstChild][opacity]=val}}else{useCssAnimations=vendor(s,animation)}})();Y.namespace('FL').Spinner=Spinner})(window,document);


}, '2.0.0' );


YUI.add('fl-utils', function(Y) {

/**
 * @module fl-utils
 */

/**
 * General helper functions for all FastLine modules.
 *
 * @namespace FL
 * @class Utils
 * @constructor
 * @static
 */
Y.namespace('FL').Utils = {

    /**
	 * Checks for support of the provided CSS property.
	 * Method adapted from: https://gist.github.com/556448
	 *
	 * @method cssSupport
	 * @param p {String} The property to check.
	 * @returns Boolean
	 */
	cssSupport: function(p)
	{
		var b = document.body || document.documentElement,
	    	s = b.style,
	    	v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms', 'Icab'],
	    	i = 0;

	    // Transform not working well in these browsers
	    if(p == 'transform' && Y.UA.gecko && Y.UA.gecko < 4) { return false; }
	    if(p == 'transform' && Y.UA.opera > 0) { return false; }
	    if(p == 'transform' && Y.UA.ie > 0 && Y.UA.ie < 10) { return false; }
	    if(p == 'transform' && navigator.userAgent.match(/Trident/)) { return false; }

	    // No css support detected
	    if(typeof s == 'undefined') { return false; }

	    // Tests for standard prop
	    if(typeof s[p] == 'string') { return true; }

	    // Tests for vendor specific prop
	    p = p.charAt(0).toUpperCase() + p.substr(1);

	    for( ; i < v.length; i++) {
			if(typeof s[v[i] + p] == 'string') { return true; }
	    }
	}
};


}, '2.0.0' );
