/*global define, window, document*/
/*jslint vars:true*/

define('vendor/sly', ['jquery', '$ready!'], function ($) {
	'use strict';
	var w = window;
	
	/*!
	 * sly 1.0.1 - 8th Jun 2013
	 * https://github.com/Darsain/sly
	 *
	 * Licensed under the MIT license.
	 * http://opensource.org/licenses/MIT
	 */
	 ;(function ($, w, undefined) {
		var pluginName = 'sly';
		var className  = 'Sly';
		var namespace  = pluginName;

		// Local WindowAnimationTiming interface
		var cAF = w.cancelAnimationFrame || w.cancelRequestAnimationFrame;
		var rAF = w.requestAnimationFrame;

		// Support indicators
		var transform, gpuAcceleration;

		// Other global values
		var $doc = $(document);
		var dragInitEvents = 'touchstart.' + namespace + ' mousedown.' + namespace;
		var dragMouseEvents = 'mousemove.' + namespace + ' mouseup.' + namespace;
		var dragTouchEvents = 'touchmove.' + namespace + ' touchend.' + namespace;
		var clickEvent = 'click.' + namespace;
		var mouseDownEvent = 'mousedown.' + namespace;

		/**
		 * Sly.
		 *
		 * @class
		 *
		 * @param {Element} frame       DOM element of sly container.
		 * @param {Object}  options     Object with options.
		 * @param {Object}  callbackMap Callbacks map.
		 */
		function Sly(frame, options, callbackMap) {
			// Extend options
			var o = $.extend({}, Sly.defaults, options);

			// Private variables
			var self = this;
			var initialized = 0;
			var parallax = isNumber(frame);

			// Frame
			var $frame = $(frame);
			var $slidee = $frame.children().eq(0);
			var frameSize = 0;
			var slideeSize = 0;
			var pos = {
				start: 0,
				center: 0,
				end: 0,
				cur: 0,
				dest: 0
			};

			// Scrollbar
			var $sb = $(o.scrollBar).eq(0);
			var $handle = $sb.children().eq(0);
			var sbSize = 0;
			var handleSize = 0;
			var hPos = {
				start: 0,
				end: 0,
				cur: 0
			};

			// Pagesbar
			var $pb = $(o.pagesBar);
			var $pages = 0;
			var pages = [];

			// Items
			var $items = 0;
			var items = [];
			var rel = {
				firstItem: 0,
				lastItem: 0,
				centerItem: 0,
				activeItem: -1,
				activePage: 0
			};

			// Navigation type booleans
			var basicNav = o.itemNav === 'basic';
			var forceCenteredNav = o.itemNav === 'forceCentered';
			var centeredNav = o.itemNav === 'centered' || forceCenteredNav;
			var itemNav = !parallax && (basicNav || centeredNav || forceCenteredNav);

			// Miscellaneous
			var $scrollSource = o.scrollSource ? $(o.scrollSource) : $frame;
			var $dragSource = o.dragSource ? $(o.dragSource) : $frame;
			var $forwardButton = $(o.forward);
			var $backwardButton = $(o.backward);
			var $prevButton = $(o.prev);
			var $nextButton = $(o.next);
			var $prevPageButton = $(o.prevPage);
			var $nextPageButton = $(o.nextPage);
			var callbacks = {};
			var last = {};
			var animation = {};
			var move = {};
			var dragging = { released: 1 };
			var renderID = 0;
			var historyID = 0;
			var cycleID = 0;
			var continuousID = 0;
			var i, l;

			// Normalizing frame
			if (!parallax) {
				frame = $frame[0];
			}

			// Expose properties
			self.frame = frame;
			self.slidee = $slidee[0];
			self.pos = pos;
			self.rel = rel;
			self.items = items;
			self.pages = pages;
			self.isPaused = 0;
			self.options = o;

			/**
			 * (Re)Loading function.
			 *
			 * Populate arrays, set sizes, bind events, ...
			 *
			 * @return {Void}
			 */
			function load() {
				// Local variables
				var ignoredMargin = 0;

				// Save old position
				pos.old = $.extend({}, pos);

				// Reset global variables
				frameSize = parallax ? 0 : $frame[o.horizontal ? 'width' : 'height']();
				sbSize = $sb[o.horizontal ? 'width' : 'height']();
				slideeSize = parallax ? frame : $slidee[o.horizontal ? 'outerWidth' : 'outerHeight']();
				pages.length = 0;

				// Set position limits & relatives
				pos.start = 0;
				pos.end = Math.max(slideeSize - frameSize, 0);
				last = {};

				// Sizes & offsets for item based navigations
				if (itemNav) {
					// Save the number of current items
					var lastItemsCount = items.length;

					// Reset itemNav related variables
					$items = $slidee.children(o.itemSelector);
					items.length = 0;

					// Needed variables
					var paddingStart = getPx($slidee, o.horizontal ? 'paddingLeft' : 'paddingTop');
					var paddingEnd = getPx($slidee, o.horizontal ? 'paddingRight' : 'paddingBottom');
					var marginStart = getPx($items, o.horizontal ? 'marginLeft' : 'marginTop');
					var marginEnd = getPx($items.slice(-1), o.horizontal ? 'marginRight' : 'marginBottom');
					var centerOffset = 0;
					var areFloated = $items.css('float') !== 'none';

					// Update ignored margin
					ignoredMargin = marginStart ? 0 : marginEnd;

					// Reset slideeSize
					slideeSize = 0;

					// Iterate through items
					$items.each(function (i, element) {
						// Item
						var $item = $(element);
						var itemSize = $item[o.horizontal ? 'outerWidth' : 'outerHeight'](true);
						var itemMarginStart = getPx($item, o.horizontal ? 'marginLeft' : 'marginTop');
						var itemMarginEnd = getPx($item, o.horizontal ? 'marginRight' : 'marginBottom');
						var itemData = {
							el: element,
							size: itemSize,
							half: itemSize / 2,
							start: slideeSize - (!i || o.horizontal ? 0 : itemMarginStart),
							center: slideeSize - Math.round(frameSize / 2 - itemSize / 2),
							end: slideeSize - frameSize + itemSize - (marginStart ? 0 : itemMarginEnd)
						};

						// Account for centerOffset & slidee padding
						if (!i) {
							centerOffset = -(forceCenteredNav ? Math.round(frameSize / 2 - itemSize / 2) : 0) + paddingStart;
							slideeSize += paddingStart;
						}

						// Increment slidee size for size of the active element
						slideeSize += itemSize;

						// Try to account for vertical margin collapsing in vertical mode
						// It's not bulletproof, but should work in 99% of cases
						if (!o.horizontal && !areFloated) {
							// Subtract smaller margin, but only when top margin is not 0, and this is not the first element
							if (itemMarginEnd && itemMarginStart && i > 0) {
								slideeSize -= Math.min(itemMarginStart, itemMarginEnd);
							}
						}

						// Things to be done on last item
						if (i === $items.length - 1) {
							slideeSize += paddingEnd;
						}

						// Add item object to items array
						items.push(itemData);
					});

					// Resize SLIDEE to fit all items
					$slidee[0].style[o.horizontal ? 'width' : 'height'] = slideeSize + 'px';

					// Adjust internal SLIDEE size for last margin
					slideeSize -= ignoredMargin;

					// Set limits
					pos.start = centerOffset;
					pos.end = forceCenteredNav ? (items.length ? items[items.length - 1].center : centerOffset) : Math.max(slideeSize - frameSize, 0);

					// Activate last item if previous active has been removed, or first item
					// when there were no items before, and new got appended.
					if (rel.activeItem >= items.length || lastItemsCount === 0 && items.length > 0) {
						activate(items.length > 0 ? items.length - 1 : 0);
					}
				}

				// Calculate SLIDEE center position
				pos.center = Math.round(pos.end / 2 + pos.start / 2);

				// Update relative positions
				updateRelatives();

				// Scrollbar
				if ($handle.length && sbSize > 0) {
					// Stretch scrollbar handle to represent the visible area
					if (o.dynamicHandle) {
						// ForceCentered navigation edge case
						if (forceCenteredNav) {
							handleSize = items.length ? sbSize * frameSize / (frameSize + items[items.length-1].center - items[0].center) : sbSize;
						} else {
							handleSize = sbSize * frameSize / slideeSize;
						}
						handleSize = within(Math.round(handleSize), o.minHandleSize, sbSize);
						$handle[0].style[o.horizontal ? 'width' : 'height'] = handleSize + 'px';
					} else {
						handleSize = $handle[o.horizontal ? 'outerWidth' : 'outerHeight']();
					}

					hPos.end = sbSize - handleSize;

					if (!renderID) {
						syncScrollbar();
					}
				}

				// Pages
				if (!parallax && frameSize > 0) {
					var tempPagePos = pos.start;
					var pagesHtml = '';

					// Populate pages array
					if (itemNav) {
						$.each(items, function (i, item) {
							if (forceCenteredNav || item.start + item.size > tempPagePos) {
								tempPagePos = item[forceCenteredNav ? 'center' : 'start'];
								pages.push(tempPagePos);
								tempPagePos += frameSize;
							}
						});
					} else {
						while (tempPagePos - frameSize < pos.end) {
							pages.push(tempPagePos);
							tempPagePos += frameSize;
						}
					}

					// Pages bar
					if ($pb[0]) {
						for (var i = 0; i < pages.length; i++) {
							pagesHtml += o.pageBuilder.call(self, i);
						}
						$pages = $pb.html(pagesHtml).children();
					}
				}

				// Fix possible overflowing
				slideTo(within(pos.dest, pos.start, pos.end));

				// Extend relative variables object with some useful info
				rel.slideeSize = slideeSize;
				rel.frameSize = frameSize;
				rel.sbSize = sbSize;
				rel.handleSize = handleSize;

				// Trigger load event
				trigger('load');
			}
			self.reload = load;

			/**
			 * Animate to a position.
			 *
			 * @param {Int}  newPos    New position.
			 * @param {Bool} immediate Reposition immediately without an animation.
			 * @param {Bool} dontAlign Do not align items, use the raw position passed in first argument.
			 *
			 * @return {Void}
			 */
			function slideTo(newPos, immediate, dontAlign) {
				// Align items
				if (itemNav && dragging.released && !dontAlign) {
					var tempRel = getRelatives(newPos);
					var isNotBordering = newPos > pos.start && newPos < pos.end;

					if (centeredNav) {
						if (isNotBordering) {
							newPos = items[tempRel.centerItem].center;
						}
						if (forceCenteredNav && o.activateMiddle) {
							activate(tempRel.centerItem);
						}
					} else if (isNotBordering) {
						newPos = items[tempRel.firstItem].start;
					}
				}

				// Handle overflowing position limits
				if (dragging.init && dragging.slidee && o.elasticBounds) {
					if (newPos > pos.end) {
						newPos = pos.end + (newPos - pos.end) / 6;
					} else if (newPos < pos.start) {
						newPos = pos.start + (newPos - pos.start) / 6;
					}
				} else {
					newPos = within(newPos, pos.start, pos.end);
				}

				// Update the animation object
				animation.start = +new Date();
				animation.time = 0;
				animation.from = pos.cur;
				animation.to = newPos;
				animation.delta = newPos - pos.cur;
				animation.tweesing = dragging.tweese || dragging.init && !dragging.slidee;
				animation.immediate = immediate || dragging.init && dragging.slidee && !dragging.tweese;

				// Reset dragging tweesing request
				dragging.tweese = 0;

				// Start animation rendering
				if (newPos !== pos.dest) {
					pos.dest = newPos;
					trigger('change');
					if (!renderID) {
						render();
					}
				}

				// Reset next cycle timeout
				resetCycle();

				// Synchronize states
				updateRelatives();
				updateButtonsState();
				syncPagesbar();
			}

			/**
			 * Render animation frame.
			 *
			 * @return {Void}
			 */
			function render() {
				// If first render call, wait for next animationFrame
				if (!renderID) {
					renderID = rAF(render);
					if (dragging.released) {
						trigger('moveStart');
					}
					return;
				}

				// If immediate repositioning is requested, don't animate.
				if (animation.immediate) {
					pos.cur = animation.to;
				}
				// Use tweesing for animations without known end point
				else if (animation.tweesing) {
					animation.tweeseDelta = animation.to - pos.cur;
					// Fuck Zeno's paradox
					if (Math.abs(animation.tweeseDelta) < 0.1) {
						pos.cur = animation.to;
					} else {
						pos.cur += animation.tweeseDelta * (dragging.released ? o.swingSpeed : o.syncSpeed);
					}
				}
				// Use tweening for basic animations with known end point
				else {
					animation.time = Math.min(+new Date() - animation.start, o.speed);
					pos.cur = animation.from + animation.delta * jQuery.easing[o.easing](animation.time/o.speed, animation.time, 0, 1, o.speed);
				}

				// If there is nothing more to render break the rendering loop, otherwise request new animation frame.
				if (animation.to === pos.cur) {
					pos.cur = animation.to;
					dragging.tweese = renderID = 0;
				} else {
					renderID = rAF(render);
				}

				trigger('move');

				// Update SLIDEE position
				if (!parallax) {
					if (transform) {
						$slidee[0].style[transform] = gpuAcceleration + (o.horizontal ? 'translateX' : 'translateY') + '(' + (-pos.cur) + 'px)';
					} else {
						$slidee[0].style[o.horizontal ? 'left' : 'top'] = -Math.round(pos.cur) + 'px';
					}
				}

				// When animation reached the end, and dragging is not active, trigger moveEnd
				if (!renderID && dragging.released) {
					trigger('moveEnd');
				}

				syncScrollbar();
			}

			/**
			 * Synchronizes scrollbar with the SLIDEE.
			 *
			 * @return {Void}
			 */
			function syncScrollbar() {
				if ($handle.length) {
					hPos.cur = pos.start === pos.end ? 0 : (((dragging.init && !dragging.slidee) ? pos.dest : pos.cur) - pos.start) / (pos.end - pos.start) * hPos.end;
					hPos.cur = within(Math.round(hPos.cur), hPos.start, hPos.end);
					if (last.hPos !== hPos.cur) {
						last.hPos = hPos.cur;
						if (transform) {
							$handle[0].style[transform] = gpuAcceleration + (o.horizontal ? 'translateX' : 'translateY') + '(' + hPos.cur + 'px)';
						} else {
							$handle[0].style[o.horizontal ? 'left' : 'top'] = hPos.cur + 'px';
						}
					}
				}
			}

			/**
			 * Synchronizes pagesbar with SLIDEE.
			 *
			 * @return {Void}
			 */
			function syncPagesbar() {
				if ($pages[0] && last.page !== rel.activePage) {
					last.page = rel.activePage;
					$pages.removeClass(o.activeClass).eq(rel.activePage).addClass(o.activeClass);
					trigger('activePage', last.page);
				}
			}

			/**
			 * Returns the position object.
			 *
			 * @param {Mixed} item
			 *
			 * @return {Object}
			 */
			self.getPos = function (item) {
				if (itemNav) {
					var index = getIndex(item);
					return index !== -1 ? items[index] : false;
				} else {
					var $item = $slidee.find(item).eq(0);

					if ($item[0]) {
						var offset = o.horizontal ? $item.offset().left - $slidee.offset().left : $item.offset().top - $slidee.offset().top;
						var size = $item[o.horizontal ? 'outerWidth' : 'outerHeight']();

						return {
							start: offset,
							center: offset - frameSize / 2 + size / 2,
							end: offset - frameSize + size,
							size: size
						};
					} else {
						return false;
					}
				}
			};

			/**
			 * Continuous move in a specified direction.
			 *
			 * @param  {Bool} forward True for forward movement, otherwise it'll go backwards.
			 * @param  {Int}  speed   Movement speed in pixels per frame. Overrides options.moveBy value.
			 *
			 * @return {Void}
			 */
			self.moveBy = function (speed) {
				move.speed = speed;
				// If already initiated, or there is nowhere to move, abort
				if (dragging.init || !move.speed || pos.cur === (move.speed > 0 ? pos.end : pos.start)) {
					return;
				}
				// Initiate move object
				move.lastTime = +new Date();
				move.startPos = pos.cur;
				// Set dragging as initiated
				continuousInit('button');
				dragging.init = 1;
				// Start movement
				trigger('moveStart');
				cAF(continuousID);
				moveLoop();
			};

			/**
			 * Continuous movement loop.
			 *
			 * @return {Void}
			 */
			function moveLoop() {
				// If there is nowhere to move anymore, stop
				if (!move.speed || pos.cur === (move.speed > 0 ? pos.end : pos.start)) {
					self.stop();
				}
				// Request new move loop if it hasn't been stopped
				continuousID = dragging.init ? rAF(moveLoop) : 0;
				// Update move object
				move.now = +new Date();
				move.pos = pos.cur + (move.now - move.lastTime) / 1000 * move.speed;
				// Slide
				slideTo(dragging.init ? move.pos : Math.round(move.pos));
				// Normally, this is triggered in render(), but if there
				// is nothing to render, we have to do it manually here.
				if (!dragging.init && pos.cur === pos.dest) {
					trigger('moveEnd');
				}
				// Update times for future iteration
				move.lastTime = move.now;
			}

			/**
			 * Stops continuous movement.
			 *
			 * @return {Void}
			 */
			self.stop = function () {
				if (dragging.source === 'button') {
					dragging.init = 0;
					dragging.released = 1;
				}
			};

			/**
			 * Activate previous item.
			 *
			 * @return {Void}
			 */
			self.prev = function () {
				self.activate(rel.activeItem - 1);
			};

			/**
			 * Activate next item.
			 *
			 * @return {Void}
			 */
			self.next = function () {
				self.activate(rel.activeItem + 1);
			};

			/**
			 * Activate previous page.
			 *
			 * @return {Void}
			 */
			self.prevPage = function () {
				self.activatePage(rel.activePage - 1);
			};

			/**
			 * Activate next page.
			 *
			 * @return {Void}
			 */
			self.nextPage = function () {
				self.activatePage(rel.activePage + 1);
			};

			/**
			 * Slide SLIDEE by amount of pixels.
			 *
			 * @param {Int}  delta     Difference in position. Positive means forward, negative means backward.
			 * @param {Bool} immediate Reposition immediately without an animation.
			 *
			 * @return {Void}
			 */
			self.slideBy = function (delta, immediate) {
				slideTo(pos.dest + delta, immediate);
			};

			/**
			 * Animate SLIDEE to a specific position.
			 *
			 * @param {Int}  pos       New position.
			 * @param {Bool} immediate Reposition immediately without an animation.
			 *
			 * @return {Void}
			 */
			self.slideTo = function (pos, immediate) {
				slideTo(pos, immediate);
			};

			/**
			 * Core method for handling `toLocation` methods.
			 *
			 * @param  {String} location
			 * @param  {Mixed}  item
			 * @param  {Bool}   immediate
			 *
			 * @return {Void}
			 */
			function to(location, item, immediate) {
				// Optional arguments logic
				if (type(item) === 'boolean') {
					immediate = item;
					item = undefined;
				}

				if (item === undefined) {
					slideTo(pos[location], immediate);
				} else {
					// You can't align items to sides of the frame
					// when centered navigation type is enabled
					if (centeredNav && location !== 'center') {
						return;
					}

					var itemPos = self.getPos(item);
					if (itemPos) {
						slideTo(itemPos[location], immediate, !centeredNav);
					}
				}
			}

			/**
			 * Animate element or the whole SLIDEE to the start of the frame.
			 *
			 * @param {Mixed} item      Item DOM element, or index starting at 0. Omitting will animate SLIDEE.
			 * @param {Bool}  immediate Reposition immediately without an animation.
			 *
			 * @return {Void}
			 */
			self.toStart = function (item, immediate) {
				to('start', item, immediate);
			};

			/**
			 * Animate element or the whole SLIDEE to the end of the frame.
			 *
			 * @param {Mixed} item      Item DOM element, or index starting at 0. Omitting will animate SLIDEE.
			 * @param {Bool}  immediate Reposition immediately without an animation.
			 *
			 * @return {Void}
			 */
			self.toEnd = function (item, immediate) {
				to('end', item, immediate);
			};

			/**
			 * Animate element or the whole SLIDEE to the center of the frame.
			 *
			 * @param {Mixed} item      Item DOM element, or index starting at 0. Omitting will animate SLIDEE.
			 * @param {Bool}  immediate Reposition immediately without an animation.
			 *
			 * @return {Void}
			 */
			self.toCenter = function (item, immediate) {
				to('center', item, immediate);
			};

			/**
			 * Get the index of an item in SLIDEE.
			 *
			 * @param {Mixed} item     Item DOM element.
			 *
			 * @return {Int}  Item index, or -1 if not found.
			 */
			function getIndex(item) {
				return type(item) !== 'undefined' ?
						isNumber(item) ?
							item >= 0 && item < items.length ? item : -1 :
							$items.index(item) :
						-1;
			}
			// Expose getIndex without lowering the compressibility of it,
			// as it is used quite often throughout Sly.
			self.getIndex = getIndex;

			/**
			 * Get index of an item in SLIDEE based on a variety of input types.
			 *
			 * @param  {Mixed} item DOM element, positive or negative integer.
			 *
			 * @return {Int}   Item index, or -1 if not found.
			 */
			function getRelativeIndex(item) {
				return getIndex(isNumber(item) && item < 0 ? item + items.length : item);
			}

			/**
			 * Activates an item.
			 *
			 * @param  {Mixed} item Item DOM element, or index starting at 0.
			 *
			 * @return {Mixed} Activated item index or false on fail.
			 */
			function activate(item) {
				var index = getIndex(item);

				if (!itemNav || index < 0) {
					return false;
				}

				// Update classes, last active index, and trigger active event only when there
				// has been a change. Otherwise just return the current active index.
				if (last.active !== index) {
					// Update classes
					$items.eq(rel.activeItem).removeClass(o.activeClass);
					$items.eq(index).addClass(o.activeClass);

					last.active = rel.activeItem = index;

					updateButtonsState();
					trigger('active', index);
				}

				return index;
			}

			/**
			 * Activates an item and helps with further navigation when o.smart is enabled.
			 *
			 * @param {Mixed} item      Item DOM element, or index starting at 0.
			 * @param {Bool}  immediate Whether to reposition immediately in smart navigation.
			 *
			 * @return {Void}
			 */
			self.activate = function (item, immediate) {
				var index = activate(item);

				// Smart navigation
				if (o.smart && index !== false) {
					// When centeredNav is enabled, center the element.
					// Otherwise, determine where to position the element based on its current position.
					// If the element is currently on the far end side of the frame, assume that user is
					// moving forward and animate it to the start of the visible frame, and vice versa.
					if (centeredNav) {
						self.toCenter(index, immediate);
					} else if (index >= rel.lastItem) {
						self.toStart(index, immediate);
					} else if (index <= rel.firstItem) {
						self.toEnd(index, immediate);
					} else {
						resetCycle();
					}
				}
			};

			/**
			 * Activates a page.
			 *
			 * @param {Int}  index     Page index, starting from 0.
			 * @param {Bool} immediate Whether to reposition immediately without animation.
			 *
			 * @return {Void}
			 */
			self.activatePage = function (index, immediate) {
				if (isNumber(index)) {
					slideTo(pages[within(index, 0, pages.length - 1)], immediate);
				}
			};

			/**
			 * Return relative positions of items based on their visibility within FRAME.
			 *
			 * @param {Int} slideePos Position of SLIDEE.
			 *
			 * @return {Void}
			 */
			function getRelatives(slideePos) {
				slideePos = within(isNumber(slideePos) ? slideePos : pos.dest, pos.start, pos.end);

				var relatives = {};
				var centerOffset = forceCenteredNav ? 0 : frameSize / 2;

				// Determine active page
				if (!parallax) {
					for (var p = 0, pl = pages.length; p < pl; p++) {
						if (slideePos >= pos.end || p === pages.length - 1) {
							relatives.activePage = pages.length - 1;
							break;
						}

						if (slideePos <= pages[p] + centerOffset) {
							relatives.activePage = p;
							break;
						}
					}
				}

				// Relative item indexes
				if (itemNav) {
					var first = false;
					var last = false;
					var center = false;

					// From start
					for (var i = 0, il = items.length; i < il; i++) {
						// First item
						if (first === false && slideePos <= items[i].start + items[i].half) {
							first = i;
						}

						// Center item
						if (center === false && slideePos <= items[i].center + items[i].half) {
							center = i;
						}

						// Last item
						if (i === il - 1 || slideePos <= items[i].end + items[i].half) {
							last = i;
							break;
						}
					}

					// Safe assignment, just to be sure the false won't be returned
					relatives.firstItem = isNumber(first) ? first : 0;
					relatives.centerItem = isNumber(center) ? center : relatives.firstItem;
					relatives.lastItem = isNumber(last) ? last : relatives.centerItem;
				}

				return relatives;
			}

			/**
			 * Update object with relative positions.
			 *
			 * @param {Int} newPos
			 *
			 * @return {Void}
			 */
			function updateRelatives(newPos) {
				$.extend(rel, getRelatives(newPos));
			}

			/**
			 * Disable navigation buttons when needed.
			 *
			 * Adds disabledClass, and when the button is <button> or <input>, activates :disabled state.
			 *
			 * @return {Void}
			 */
			function updateButtonsState() {
				var isStart = pos.dest <= pos.start;
				var isEnd = pos.dest >= pos.end;
				var slideePosState = isStart ? 1 : isEnd ? 2 : 3;

				// Update paging buttons only if there has been a change in SLIDEE position
				if (last.slideePosState !== slideePosState) {
					last.slideePosState = slideePosState;

					if ($prevPageButton.is('button,input')) {
						$prevPageButton.prop('disabled', isStart);
					}

					if ($nextPageButton.is('button,input')) {
						$nextPageButton.prop('disabled', isEnd);
					}

					$prevPageButton.add($backwardButton)[isStart ? 'addClass' : 'removeClass'](o.disabledClass);
					$nextPageButton.add($forwardButton)[isEnd ? 'addClass' : 'removeClass'](o.disabledClass);
				}

				// Forward & Backward buttons need a separate state caching because we cannot "property disable"
				// them while they are being used, as disabled buttons stop emitting mouse events.
				if (last.fwdbwdState !== slideePosState && dragging.released) {
					last.fwdbwdState = slideePosState;

					if ($backwardButton.is('button,input')) {
						$backwardButton.prop('disabled', isStart);
					}

					if ($forwardButton.is('button,input')) {
						$forwardButton.prop('disabled', isEnd);
					}
				}

				// Item navigation
				if (itemNav) {
					var isFirst = rel.activeItem === 0;
					var isLast = rel.activeItem >= items.length - 1;
					var itemsButtonState = isFirst ? 1 : isLast ? 2 : 3;

					if (last.itemsButtonState !== itemsButtonState) {
						last.itemsButtonState = itemsButtonState;

						if ($prevButton.is('button,input')) {
							$prevButton.prop('disabled', isFirst);
						}

						if ($nextButton.is('button,input')) {
							$nextButton.prop('disabled', isLast);
						}

						$prevButton[isFirst ? 'addClass' : 'removeClass'](o.disabledClass);
						$nextButton[isLast ? 'addClass' : 'removeClass'](o.disabledClass);
					}
				}
			}

			/**
			 * Resume cycling.
			 *
			 * @param {Int} priority Resume pause with priority lower or equal than this. Used internally for pauseOnHover.
			 *
			 * @return {Void}
			 */
			self.resume = function (priority) {
				if (!o.cycleBy || !o.cycleInterval || o.cycleBy === 'items' && !items[0] || priority < self.isPaused) {
					return;
				}

				self.isPaused = 0;

				if (cycleID) {
					cycleID = clearTimeout(cycleID);
				} else {
					trigger('resume');
				}

				cycleID = setTimeout(function () {
					trigger('cycle');
					switch (o.cycleBy) {
						case 'items':
							self.activate(rel.activeItem >= items.length - 1 ? 0 : rel.activeItem + 1);
							break;

						case 'pages':
							self.activatePage(rel.activePage >= pages.length - 1 ? 0 : rel.activePage + 1);
							break;
					}
				}, o.cycleInterval);
			};

			/**
			 * Pause cycling.
			 *
			 * @param {Int} priority Pause priority. 100 is default. Used internally for pauseOnHover.
			 *
			 * @return {Void}
			 */
			self.pause = function (priority) {
				if (priority < self.isPaused) {
					return;
				}

				self.isPaused = priority || 100;

				if (cycleID) {
					cycleID = clearTimeout(cycleID);
					trigger('pause');
				}
			};

			/**
			 * Toggle cycling.
			 *
			 * @return {Void}
			 */
			self.toggle = function () {
				self[cycleID ? 'pause' : 'resume']();
			};

			/**
			 * Updates a signle or multiple option values.
			 *
			 * @param {Mixed} name  Name of the option that should be updated, or object that will extend the options.
			 * @param {Mixed} value New option value.
			 *
			 * @return {Void}
			 */
			self.set = function (name, value) {
				if ($.isPlainObject(name)) {
					$.extend(o, name);
				} else if (o.hasOwnProperty(name)) {
					o[name] = value;
				}
			};

			/**
			 * Add one or multiple items to the SLIDEE end, or a specified position index.
			 *
			 * @param {Mixed} element Node element, or HTML string.
			 * @param {Int}   index   Index of a new item position. By default item is appended at the end.
			 *
			 * @return {Void}
			 */
			self.add = function (element, index) {
				var $element = $(element);

				if (itemNav) {
					// Insert the element(s)
					if (type(index) === 'undefined' || !items[0]) {
						$element.appendTo($slidee);
					} else if (items.length) {
						$element.insertBefore(items[index].el);
					}

					// Adjust the activeItem index
					if (index <= rel.activeItem) {
						last.active = rel.activeItem += $element.length;
					}
				} else {
					$slidee.append($element);
				}

				// Reload
				load();
			};

			/**
			 * Remove an item from SLIDEE.
			 *
			 * @param {Mixed} element Item index, or DOM element.
			 * @param {Int}   index   Index of a new item position. By default item is appended at the end.
			 *
			 * @return {Void}
			 */
			self.remove = function (element) {
				if (itemNav) {
					var index = getRelativeIndex(element);

					if (index > -1) {
						// Remove the element
						$items.eq(index).remove();

						// If the current item is being removed, activate new one after reload
						var reactivate = index === rel.activeItem && !(forceCenteredNav && o.activateMiddle);

						// Adjust the activeItem index
						if (index < rel.activeItem || rel.activeItem >= items.length - 1) {
							last.active = --rel.activeItem;
						}

						// Reload
						load();

						// Activate new item at the removed position if the current active got removed
						if (reactivate) {
							self.activate(rel.activeItem);
						}
					}
				} else {
					$(element).remove();
					load();
				}
			};

			/**
			 * Helps re-arranging items.
			 *
			 * @param  {Mixed} item     Item DOM element, or index starting at 0. Use negative numbers to select items from the end.
			 * @param  {Mixed} position Item insertion anchor. Accepts same input types as item argument.
			 * @param  {Bool}  after    Insert after instead of before the anchor.
			 *
			 * @return {Void}
			 */
			function moveItem(item, position, after) {
				item = getRelativeIndex(item);
				position = getRelativeIndex(position);

				// Move only if there is an actual change requested
				if (item > -1 && position > -1 && item !== position && (!after || position !== item - 1) && (after || position !== item + 1)) {
					$items.eq(item)[after ? 'insertAfter' : 'insertBefore'](items[position].el);

					var shiftStart = item < position ? item : (after ? position : position - 1);
					var shiftEnd = item > position ? item : (after ? position + 1 : position);
					var shiftsUp = item > position;

					// Update activeItem index
					if (item === rel.activeItem) {
						last.active = rel.activeItem = after ? (shiftsUp ? position + 1 : position) : (shiftsUp ? position : position - 1);
					} else if (rel.activeItem > shiftStart && rel.activeItem < shiftEnd) {
						last.active = rel.activeItem += shiftsUp ? 1 : -1;
					}

					// Reload
					load();
				}
			}

			/**
			 * Move item after the target anchor.
			 *
			 * @param  {Mixed} item     Item to be moved. Can be DOM element or item index.
			 * @param  {Mixed} position Target position anchor. Can be DOM element or item index.
			 *
			 * @return {Void}
			 */
			self.moveAfter = function (item, position) {
				moveItem(item, position, 1);
			};

			/**
			 * Move item before the target anchor.
			 *
			 * @param  {Mixed} item     Item to be moved. Can be DOM element or item index.
			 * @param  {Mixed} position Target position anchor. Can be DOM element or item index.
			 *
			 * @return {Void}
			 */
			self.moveBefore = function (item, position) {
				moveItem(item, position);
			};

			/**
			 * Registers callbacks.
			 *
			 * @param  {Mixed} name  Event name, or callbacks map.
			 * @param  {Mixed} fn    Callback, or an array of callback functions.
			 *
			 * @return {Void}
			 */
			self.on = function (name, fn) {
				// Callbacks map
				if (type(name) === 'object') {
					for (var key in name) {
						if (name.hasOwnProperty(key)) {
							self.on(key, name[key]);
						}
					}
				// Callback
				} else if (type(fn) === 'function') {
					var names = name.split(' ');
					for (var n = 0, nl = names.length; n < nl; n++) {
						callbacks[names[n]] = callbacks[names[n]] || [];
						if (callbackIndex(names[n], fn) === -1) {
							callbacks[names[n]].push(fn);
						}
					}
				// Callbacks array
				} else if (type(fn) === 'array') {
					for (var f = 0, fl = fn.length; f < fl; f++) {
						self.on(name, fn[f]);
					}
				}
			};

			/**
			 * Remove one or all callbacks.
			 *
			 * @param  {String} name Event name.
			 * @param  {Mixed}  fn   Callback, or an array of callback functions. Omit to remove all callbacks.
			 *
			 * @return {Void}
			 */
			self.off = function (name, fn) {
				if (fn instanceof Array) {
					for (var f = 0, fl = fn.length; f < fl; f++) {
						self.off(name, fn[f]);
					}
				} else {
					var names = name.split(' ');
					for (var n = 0, nl = names.length; n < nl; n++) {
						callbacks[names[n]] = callbacks[names[n]] || [];
						if (type(fn) === 'undefined') {
							callbacks[names[n]].length = 0;
						} else {
							var index = callbackIndex(names[n], fn);
							if (index !== -1) {
								callbacks[names[n]].splice(index, 1);
							}
						}
					}
				}
			};

			/**
			 * Returns callback array index.
			 *
			 * @param  {String}   name Event name.
			 * @param  {Function} fn   Function
			 *
			 * @return {Int} Callback array index, or -1 if isn't registered.
			 */
			function callbackIndex(name, fn) {
				for (var i = 0, l = callbacks[name].length; i < l; i++) {
					if (callbacks[name][i] === fn) {
						return i;
					}
				}
				return -1;
			}

			/**
			 * Reset next cycle timeout.
			 *
			 * @return {Void}
			 */
			function resetCycle() {
				if (dragging.released && !self.isPaused) {
					self.resume();
				}
			}

			/**
			 * Calculate SLIDEE representation of handle position.
			 *
			 * @param  {Int} handlePos
			 *
			 * @return {Int}
			 */
			function handleToSlidee(handlePos) {
				return Math.round(within(handlePos, hPos.start, hPos.end) / hPos.end * (pos.end - pos.start)) + pos.start;
			}

			/**
			 * Keeps track of a dragging delta history.
			 *
			 * @return {Void}
			 */
			function draggingHistoryTick() {
				// Looking at this, I know what you're thinking :) But as we need only 4 history states, doing it this way
				// as opposed to a proper loop is ~25 bytes smaller (when minified with GCC), a lot faster, and doesn't
				// generate garbage. The loop version would create 2 new variables on every tick. Unexaptable!
				dragging.history[0] = dragging.history[1];
				dragging.history[1] = dragging.history[2];
				dragging.history[2] = dragging.history[3];
				dragging.history[3] = dragging.delta;
			}

			/**
			 * Initialize continuous movement.
			 *
			 * @return {Void}
			 */
			function continuousInit(source) {
				dragging.released = 0;
				dragging.source = source;
				dragging.slidee = source === 'slidee';
			}

			/**
			 * Dragging initiator.
			 *
			 * @param  {Event} event
			 *
			 * @return {Void}
			 */
			function dragInit(event) {
				// Ignore when already in progress
				if (dragging.init) {
					return;
				}

				var isTouch = event.type === 'touchstart';
				var source = event.data.source;
				var isSlidee = source === 'slidee';

				// Handle dragging conditions
				if (source === 'handle' && (!o.dragHandle || hPos.start === hPos.end)) {
					return;
				}

				// SLIDEE dragging conditions
				if (isSlidee && !(isTouch ? o.touchDragging : o.mouseDragging && event.which < 2)) {
					return;
				}

				if (!isTouch) {
					stopDefault(event, 1);
				}

				// Reset dragging object
				continuousInit(source);

				// Properties used in dragHandler
				dragging.$source = $(event.target);
				dragging.init = 0;
				dragging.touch = isTouch;
				dragging.pointer = isTouch ? event.originalEvent.touches[0] : event;
				dragging.initX = dragging.pointer.pageX;
				dragging.initY = dragging.pointer.pageY;

				dragging.initPos = isSlidee ? pos.cur : hPos.cur;
				dragging.start = +new Date();
				dragging.time = 0;
				dragging.path = 0;
				dragging.pathToInit = isSlidee ? isTouch ? 50 : 10 : 0;
				dragging.history = [0, 0, 0, 0];
				dragging.initLoc = dragging[o.horizontal ? 'initX' : 'initY'];
				dragging.deltaMin = isSlidee ? -dragging.initLoc : -hPos.cur;
				dragging.deltaMax = isSlidee ? document[o.horizontal ? 'width' : 'height'] - dragging.initLoc : hPos.end - hPos.cur;

				// Add dragging class
				(isSlidee ? $slidee : $handle).addClass(o.draggedClass);

				// Bind dragging events
				$doc.on(isTouch ? dragTouchEvents : dragMouseEvents, dragHandler);

				// Keep track of a dragging path history. This is later used in the
				// dragging release swing calculation when dragging SLIDEE.
				if (isSlidee) {
					historyID = setInterval(draggingHistoryTick, 10);
				}
			}

			/**
			 * Handler for dragging scrollbar handle or SLIDEE.
			 *
			 * @param  {Event} event
			 *
			 * @return {Void}
			 */
			function dragHandler(event) {
				dragging.released = event.type === 'mouseup' || event.type === 'touchend';
				dragging.pointer = dragging.touch ? event.originalEvent[dragging.released ? 'changedTouches' : 'touches'][0] : event;
				dragging.pathX = dragging.pointer.pageX - dragging.initX;
				dragging.pathY = dragging.pointer.pageY - dragging.initY;
				dragging.pathTotal = Math.sqrt(Math.pow(dragging.pathX, 2) + Math.pow(dragging.pathY, 2));
				dragging.delta = within(o.horizontal ? dragging.pathX : dragging.pathY, dragging.deltaMin, dragging.deltaMax);

				// Initialization
				if (!dragging.init && dragging.pathTotal > dragging.pathToInit) {
					if (dragging.slidee) {
						// If path has reached the pathToInit value, but in a wrong direction, cancel dragging
						if (o.horizontal ? Math.abs(dragging.pathX) < Math.abs(dragging.pathY) : Math.abs(dragging.pathX) > Math.abs(dragging.pathY)) {
							dragEnd();
							return;
						}
						// Disable click on a source element, as it is unwelcome when dragging SLIDEE
						dragging.$source.on(clickEvent, disableOneEvent);
					}
					// Mark dragging as initiated
					dragging.init = 1;
					// Pause ongoing cycle
					self.pause(1);
					// Trigger moveStart event
					trigger('moveStart');
				}

				// Proceed when initialized
				if (dragging.init) {
					if (dragging.released) {
						if (!dragging.touch) {
							stopDefault(event);
						}

						dragging.init = 0;

						// Adjust path with a swing on mouse release
						if (o.releaseSwing && dragging.slidee) {
							dragging.swing = (dragging.delta - dragging.history[0]) / 40 * 300;
							dragging.delta += dragging.swing;
							dragging.tweese = Math.abs(dragging.swing) > 10;
						}
					} else {
						stopDefault(event);
					}

					slideTo(dragging.slidee ? Math.round(dragging.initPos - dragging.delta) : handleToSlidee(dragging.initPos + dragging.delta));
				}

				// Stop and cleanup after dragging
				if (dragging.released) {
					dragEnd();
				}
			}

			/**
			 * Stops dragging and cleans up after it.
			 *
			 * @return {Void}
			 */
			function dragEnd() {
				clearInterval(historyID);
				$doc.off(dragging.touch ? dragTouchEvents : dragMouseEvents, dragHandler);
				(dragging.slidee ? $slidee : $handle).removeClass(o.draggedClass);

				// Resume ongoing cycle
				self.resume(1);

				// Normally, this is triggered in render(), but if there
				// is nothing to render, we have to do it manually here.
				if (dragging.init && pos.cur === pos.dest) {
					trigger('moveEnd');
				}
			}

			/**
			 * Continuous movement cleanup on mouseup.
			 *
			 * @return {Void}
			 */
			function movementReleaseHandler() {
				self.stop();
				$doc.off('mouseup', movementReleaseHandler);
			}

			/**
			 * Buttons navigation handler.
			 *
			 * @param  {Event} event
			 *
			 * @return {Void}
			 */
			function buttonsHandler(event) {
				/*jshint validthis:true */
				stopDefault(event);
				switch (this) {
					case $forwardButton[0]:
					case $backwardButton[0]:
						self.moveBy($forwardButton.is(this) ? o.moveBy : -o.moveBy);
						$doc.on('mouseup', movementReleaseHandler);
						break;

					case $prevButton[0]:
						self.prev();
						break;

					case $nextButton[0]:
						self.next();
						break;

					case $prevPageButton[0]:
						self.prevPage();
						break;

					case $nextPageButton[0]:
						self.nextPage();
						break;
				}
			}

			/**
			 * Mouse wheel delta normalization.
			 *
			 * @param  {Event} event
			 *
			 * @return {Int}
			 */
			function normalizeWheelDelta(event) {
				return within(-event.wheelDelta || event.detail, -1, 1);
			}

			/**
			 * Mouse scrolling handler.
			 *
			 * @param  {Event} event
			 *
			 * @return {Void}
			 */
			function scrollHandler(event) {
				// Ignore if there is no scrolling to be done
				if (!o.scrollBy || pos.start === pos.end) {
					return;
				}

				stopDefault(event, 1);

				var delta = normalizeWheelDelta(event.originalEvent);
				if (itemNav) {
					self[centeredNav ? 'toCenter' : 'toStart'](
						within((centeredNav ? rel.centerItem : rel.firstItem) + o.scrollBy * delta, 0, items.length)
					);
				} else {
					self.slideBy(o.scrollBy * delta);
				}
			}

			/**
			 * Scrollbar click handler.
			 *
			 * @param  {Event} event
			 *
			 * @return {Void}
			 */
			function scrollbarHandler(event) {
				// Only clicks on scroll bar. Ignore the handle.
				if (o.clickBar && event.target === $sb[0]) {
					stopDefault(event);
					// Calculate new handle position and sync SLIDEE to it
					slideTo(handleToSlidee((o.horizontal ? event.pageX - $sb.offset().left : event.pageY - $sb.offset().top) - handleSize / 2));
				}
			}

			/**
			 * Keyboard input handler.
			 *
			 * @param  {Event} event
			 *
			 * @return {Void}
			 */
			function keyboardHandler(event) {
				if (!o.keyboardNavBy) {
					return;
				}

				switch (event.which) {
					// Left or Up
					case o.horizontal ? 37 : 38:
						stopDefault(event);
						self[o.keyboardNavBy === 'pages' ? 'prevPage' : 'prev']();
						break;

					// Right or Down
					case o.horizontal ? 39 : 40:
						stopDefault(event);
						self[o.keyboardNavBy === 'pages' ? 'nextPage' : 'next']();
						break;
				}
			}

			/**
			 * Click on item activation handler.
			 *
			 * @param  {Event} event
			 *
			 * @return {Void}
			 */
			function activateHandler() {
				/*jshint validthis:true */
				// Accept only events from direct SLIDEE children.
				if (this.parentNode === $slidee[0]) {
					self.activate(this);
				}
			}

			/**
			 * Click on page button handler.
			 *
			 * @param {Event} event
			 *
			 * @return {Void}
			 */
			function activatePageHandler() {
				/*jshint validthis:true */
				// Accept only events from direct pages bar children.
				if (this.parentNode === $pb[0]) {
					self.activatePage($pages.index(this));
				}
			}

			/**
			 * Pause on hover handler.
			 *
			 * @param  {Event} event
			 *
			 * @return {Void}
			 */
			function pauseOnHoverHandler(event) {
				if (o.pauseOnHover) {
					self[event.type === 'mouseenter' ? 'pause' : 'resume'](2);
				}
			}

			/**
			 * Trigger callbacks for event.
			 *
			 * @param  {String} name Event name.
			 * @param  {Mixed}  argX Arguments passed to callbacks.
			 *
			 * @return {Void}
			 */
			function trigger(name, arg1) {
				if (callbacks[name]) {
					for (i = 0, l = callbacks[name].length; i < l; i++) {
						callbacks[name][i].call(self, name, arg1);
					}
				}
			}

			/**
			 * Destroys instance and everything it created.
			 *
			 * @return {Void}
			 */
			self.destroy = function () {
				// Unbind all events
				$doc
					.add($scrollSource)
					.add($handle)
					.add($sb)
					.add($pb)
					.add($forwardButton)
					.add($backwardButton)
					.add($prevButton)
					.add($nextButton)
					.add($prevPageButton)
					.add($nextPageButton)
					.unbind('.' + namespace);

				// Remove classes
				$prevButton
					.add($nextButton)
					.add($prevPageButton)
					.add($nextPageButton)
					.removeClass(o.disabledClass);

				if ($items) {
					$items.eq(rel.activeItem).removeClass(o.activeClass);
				}

				// Remove page items
				$pb.empty();

				if (!parallax) {
					// Unbind events from frame
					$frame.unbind('.' + namespace);
					// Reset SLIDEE and handle positions
					$slidee.add($handle).css(transform || (o.horizontal ? 'left' : 'top'), transform ? 'none' : 0);
					// Remove the instance from element data storage
					$.removeData(frame, namespace);
				}

				// Reset initialized status and return the instance
				initialized = 0;
				return self;
			};

			/**
			 * Initialize.
			 *
			 * @return {Object}
			 */
			self.init = function () {
				if (initialized) {
					return;
				}

				// Register callbacks map
				self.on(callbackMap);

				// Set required styles
				var $movables = $handle;
				if (!parallax) {
					$movables = $movables.add($slidee);
					$frame.css('overflow', 'hidden');
					if (!transform && $frame.css('position') === 'static') {
						$frame.css('position', 'relative');
					}
				}
				if (transform) {
					if (gpuAcceleration) {
						$movables.css(transform, gpuAcceleration);
					}
				} else {
					if ($sb.css('position') === 'static') {
						$sb.css('position', 'relative');
					}
					$movables.css({ position: 'absolute' });
				}

				// Navigation buttons
				if (o.forward) {
					$forwardButton.on(mouseDownEvent, buttonsHandler);
				}
				if (o.backward) {
					$backwardButton.on(mouseDownEvent, buttonsHandler);
				}
				if (o.prev) {
					$prevButton.on(clickEvent, buttonsHandler);
				}
				if (o.next) {
					$nextButton.on(clickEvent, buttonsHandler);
				}
				if (o.prevPage) {
					$prevPageButton.on(clickEvent, buttonsHandler);
				}
				if (o.nextPage) {
					$nextPageButton.on(clickEvent, buttonsHandler);
				}

				// Scrolling navigation
				$scrollSource.on('DOMMouseScroll.' + namespace + ' mousewheel.' + namespace, scrollHandler);

				// Clicking on scrollbar navigation
				if ($sb[0]) {
					$sb.on(clickEvent, scrollbarHandler);
				}

				// Click on items navigation
				if (itemNav && o.activateOn) {
					$frame.on(o.activateOn + '.' + namespace, '*', activateHandler);
				}

				// Pages navigation
				if ($pb[0] && o.activatePageOn) {
					$pb.on(o.activatePageOn + '.' + namespace, '*', activatePageHandler);
				}

				// Dragging navigation
				$dragSource.on(dragInitEvents, { source: 'slidee' }, dragInit);

				// Scrollbar dragging navigation
				if ($handle) {
					$handle.on(dragInitEvents, { source: 'handle' }, dragInit);
				}

				// Keyboard navigation
				$doc.bind('keydown.' + namespace, keyboardHandler);

				if (!parallax) {
					// Pause on hover
					$frame.on('mouseenter.' + namespace + ' mouseleave.' + namespace, pauseOnHoverHandler);
					// Reset native FRAME element scroll
					$frame.on('scroll.' + namespace, resetScroll);
				}

				// Load
				load();

				// Activate requested position
				if (itemNav) {
					activate(o.startAt);
					self[centeredNav ? 'toCenter' : 'toStart'](o.startAt);
				} else {
					slideTo(o.startAt, 1);
				}

				// Initiate automatic cycling
				if (o.cycleBy && !parallax) {
					self[o.startPaused ? 'pause' : 'resume']();
				}

				// Mark instance as initialized
				initialized = 1;

				// Return instance
				return self;
			};
		}

		/**
		 * Return type of the value.
		 *
		 * @param  {Mixed} value
		 *
		 * @return {String}
		 */
		function type(value) {
			if (value == null) {
				return String(value);
			}

			if (typeof value === 'object' || typeof value === 'function') {
				return Object.prototype.toString.call(value).match(/\s([a-z]+)/i)[1].toLowerCase() || 'object';
			}

			return typeof value;
		}

		/**
		 * Event preventDefault & stopPropagation helper.
		 *
		 * @param {Event} event     Event object.
		 * @param {Bool}  noBubbles Cancel event bubbling.
		 *
		 * @return {Void}
		 */
		function stopDefault(event, noBubbles) {
			event.preventDefault();
			if (noBubbles) {
				event.stopPropagation();
			}
		}

		/**
		 * Disables an event it was triggered on and unbinds itself.
		 *
		 * @param  {Event} event
		 *
		 * @return {Void}
		 */
		function disableOneEvent(event) {
			/*jshint validthis:true */
			stopDefault(event, 1);
			$(this).off(event.type, disableOneEvent);
		}

		/**
		 * Resets native element scroll values to 0.
		 *
		 * @return {Void}
		 */
		function resetScroll() {
			/*jshint validthis:true */
			this.scrollLeft = 0;
			this.scrollTop = 0;
		}

		/**
		 * Check if variable is a number.
		 *
		 * @param {Mixed} value
		 *
		 * @return {Boolean}
		 */
		function isNumber(value) {
			return !isNaN(parseFloat(value)) && isFinite(value);
		}

		/**
		 * Parse style to pixels.
		 *
		 * @param {Object}   $item    jQuery object with element.
		 * @param {Property} property CSS property to get the pixels from.
		 *
		 * @return {Int}
		 */
		function getPx($item, property) {
			return parseInt($item.css(property), 10) || 0;
		}

		/**
		 * Make sure that number is within the limits.
		 *
		 * @param {Number} number
		 * @param {Number} min
		 * @param {Number} max
		 *
		 * @return {Number}
		 */
		function within(number, min, max) {
			return number < min ? min : number > max ? max : number;
		}

		// Local WindowAnimationTiming interface polyfill
		(function (w) {
			var vendors = ['moz', 'webkit', 'o'];
			var lastTime = 0;

			// For a more accurate WindowAnimationTiming interface implementation, ditch the native
			// requestAnimationFrame when cancelAnimationFrame is not present (older versions of Firefox)
			for(var i = 0, l = vendors.length; i < l && !cAF; ++i) {
				cAF = w[vendors[i]+'CancelAnimationFrame'] || w[vendors[i]+'CancelRequestAnimationFrame'];
				rAF = cAF && w[vendors[i]+'RequestAnimationFrame'];
			}

			if (!cAF) {
				rAF = function (callback) {
					var currTime = +new Date();
					var timeToCall = Math.max(0, 16 - (currTime - lastTime));
					lastTime = currTime + timeToCall;
					return w.setTimeout(function () { callback(currTime + timeToCall); }, timeToCall);
				};

				cAF = function (id) {
					clearTimeout(id);
				};
			}
		}(window));

		// Feature detects
		(function () {
			var prefixes = ['', 'webkit', 'moz', 'ms', 'o'];
			var el = document.createElement('div');

			function testProp(prop) {
				for (var p = 0, pl = prefixes.length; p < pl; p++) {
					var prefixedProp = prefixes[p] ? prefixes[p] + prop.charAt(0).toUpperCase() + prop.slice(1) : prop;
					if (el.style[prefixedProp] !== undefined) {
						return prefixedProp;
					}
				}
			}

			// Global support indicators
			transform = testProp('transform');
			gpuAcceleration = testProp('perspective') ? 'translateZ(0) ' : '';
		}());

		// Expose class globally
		w[className] = Sly;

		// jQuery proxy
		$.fn[pluginName] = function (options, callbackMap) {
			var method, methodArgs;

			// Attributes logic
			if (!$.isPlainObject(options)) {
				if (type(options) === 'string' || options === false) {
					method = options === false ? 'destroy' : options;
					methodArgs = Array.prototype.slice.call(arguments, 1);
				}
				options = {};
			}

			// Apply to all elements
			return this.each(function (i, element) {
				// Call with prevention against multiple instantiations
				var plugin = $.data(element, namespace);

				if (!plugin && !method) {
					// Create a new object if it doesn't exist yet
					plugin = $.data(element, namespace, new Sly(element, options, callbackMap).init());
				} else if (plugin && method) {
					// Call method
					if (plugin[method]) {
						plugin[method].apply(plugin, methodArgs);
					}
				}
			});
		};

		// Default options
		Sly.defaults = {
			horizontal: 0, // Switch to horizontal mode.

			// Item based navigation
			itemNav:      null, // Item navigation type. Can be: 'basic', 'centered', 'forceCentered'.
			itemSelector: null, // Select only items that match this selector.
			smart:        0,    // Repositions the activated item to help with further navigation.
			activateOn:   null, // Activate an item on this event. Can be: 'click', 'mouseenter', ...
			activateMiddle: 0,  // Always activate the item in the middle of the FRAME. forceCentered only.

			// Scrolling
			scrollSource: null, // Element for catching the mouse wheel scrolling. Default is FRAME.
			scrollBy:     0,    // Pixels or items to move per one mouse scroll. 0 to disable scrolling.

			// Dragging
			dragSource:    null, // Selector or DOM element for catching dragging events. Default is FRAME.
			mouseDragging: 0,    // Enable navigation by dragging the SLIDEE with mouse cursor.
			touchDragging: 0,    // Enable navigation by dragging the SLIDEE with touch events.
			releaseSwing:  0,    // Ease out on dragging swing release.
			swingSpeed:    0.2,  // Swing synchronization speed, where: 1 = instant, 0 = infinite.
			elasticBounds: 0,    // Stretch SLIDEE position limits when dragging past FRAME boundaries.

			// Scrollbar
			scrollBar:     null, // Selector or DOM element for scrollbar container.
			dragHandle:    0,    // Whether the scrollbar handle should be draggable.
			dynamicHandle: 0,    // Scrollbar handle represents the ratio between hidden and visible content.
			minHandleSize: 50,   // Minimal height or width (depends on sly direction) of a handle in pixels.
			clickBar:      0,    // Enable navigation by clicking on scrollbar.
			syncSpeed:     0.5,  // Handle => SLIDEE synchronization speed, where: 1 = instant, 0 = infinite.

			// Pagesbar
			pagesBar:       null, // Selector or DOM element for pages bar container.
			activatePageOn: null, // Event used to activate page. Can be: click, mouseenter, ...
			pageBuilder:          // Page item generator.
				function (index) {
					return '<li>' + (index + 1) + '</li>';
				},

			// Navigation buttons
			forward:  null, // Selector or DOM element for "forward movement" button.
			backward: null, // Selector or DOM element for "backward movement" button.
			prev:     null, // Selector or DOM element for "previous item" button.
			next:     null, // Selector or DOM element for "next item" button.
			prevPage: null, // Selector or DOM element for "previous page" button.
			nextPage: null, // Selector or DOM element for "next page" button.

			// Automated cycling
			cycleBy:       null, // Enable automatic cycling by 'items' or 'pages'.
			cycleInterval: 5000, // Delay between cycles in milliseconds.
			pauseOnHover:  0,    // Pause cycling when mouse hovers over the FRAME.
			startPaused:   0,    // Whether to start in paused sate.

			// Mixed options
			moveBy:        300,     // Speed in pixels per second used by forward and backward buttons.
			speed:         0,       // Animations speed in milliseconds. 0 to disable animations.
			easing:        'swing', // Easing for duration based (tweening) animations.
			startAt:       0,       // Starting offset in pixels or items.
			keyboardNavBy: null,    // Enable keyboard navigation by 'items' or 'pages'.

			// Classes
			draggedClass:  'dragged',  // Class for dragged elements (like SLIDEE or scrollbar handle).
			activeClass:   'active',   // Class for active items and pages.
			disabledClass: 'disabled'  // Class for disabled navigation elements.
		};
	}($, window));

	return Sly;

});