/**
 * The Shadowbox class.
 *
 * This file is part of Shadowbox.
 *
 * Shadowbox is an online media viewer application that supports all of the
 * web's most popular media publishing formats. Shadowbox is written entirely
 * in JavaScript and CSS and is highly customizable. Using Shadowbox, website
 * authors can showcase a wide assortment of media in all major browsers without
 * navigating users away from the linking page.
 *
 * Shadowbox is released under version 3.0 of the Creative Commons Attribution-
 * Noncommercial-Share Alike license. This means that it is absolutely free
 * for personal, noncommercial use provided that you 1) make attribution to the
 * author and 2) release any derivative work under the same or a similar
 * license.
 *
 * If you wish to use Shadowbox for commercial purposes, licensing information
 * can be found at http://mjijackson.com/shadowbox/.
 *
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @copyright   2007-2008 Michael J. I. Jackson
 * @license     http://creativecommons.org/licenses/by-nc-sa/3.0/
 * @version     SVN: $Id: shadowbox.js 113 2008-09-23 03:00:36Z mjijackson $
 */

if(typeof Shadowbox == 'undefined'){
    throw 'Unable to load Shadowbox, no base library adapter found';
}

/**
 * The Shadowbox class. Used to display different media on a web page using a
 * Lightbox-like effect.
 *
 * Useful resources:
 *
 * - http://www.alistapart.com/articles/byebyeembed
 * - http://www.w3.org/TR/html401/struct/objects.html
 * - http://www.dyn-web.com/dhtml/iframes/
 * - http://www.apple.com/quicktime/player/specs.html
 * - http://www.apple.com/quicktime/tutorials/embed2.html
 * - http://www.howtocreate.co.uk/wrongWithIE/?chapter=navigator.plugins
 * - http://msdn.microsoft.com/en-us/library/ms532969.aspx
 * - http://support.microsoft.com/kb/316992
 * - http://developer.mozilla.org/en/Canvas_tutorial/Drawing_shapes
 * - http://snook.ca/archives/html_and_css/rounded_corners_experiment_ie/
 *
 * @class       Shadowbox
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @singleton
 */
(function(){

    /**
     * The current version of Shadowbox.
     *
     * @var         String
     * @private
     */
    var version = '2.1';

    /**
     * Contains the default options for Shadowbox.
     *
     * @var         Object
     * @private
     */
    var options = {
        animate: true,              // Enable all animations, except for fades
        animateFade: true,          // Enable fade animations
        animSequence: 'sync',       // Sequence of the height/width animations.
                                    // May be 'wh' (width then height), 'hw'
                                    // (height then width), or 'sync' (both at
                                    // the same time). Ignored if animate is
                                    // false
        autoplayMovies: true,       // Automatically play movies
        autoDimensions: false,      // Use the dimensions of the first piece as
                                    // the initial dimensions (if they are
                                    // available)
        continuous: false,          // Enables continuous galleries. When enabled,
                                    // user will be able to skip to the first
                                    // gallery item from the last using next and
                                    // vice versa
        counterLimit: 10,           // Limit to the number of counter links that
                                    // are displayed in a "skip" style counter
        counterType: 'default',     // Counter type. May be either "default" or
                                    // "skip". Skip counter displays a link for
                                    // each item in gallery
        displayCounter: true,       // Display the gallery counter
        displayNav: true,           // Show the navigation controls
        endlessPaging: true,        // Is there a paging before 1

        /**
         * Easing function used for animations. Based on a cubic polynomial.
         *
         * @param   Number      x       The state of the animation (% complete)
         * @return  Number              The adjusted easing value
         */
        ease: function(x){
            return 1 + Math.pow(x - 1, 3);
        },

        enableKeys: true,           // Enable keyboard navigation

        /**
         * An object containing names of plugins and links to their respective
         * download pages.
         */
        errors: {
            fla:        {
                name:   'Flash',
                url:    'http://www.adobe.com/products/flashplayer/'
            },
            qt:         {
                name:   'QuickTime',
                url:    'http://www.apple.com/quicktime/download/'
            },
            wmp:        {
                name:   'Windows Media Player',
                url:    'http://www.microsoft.com/windows/windowsmedia/'
            },
            f4m:        {
                name:   'Flip4Mac',
                url:    'http://www.flip4mac.com/wmv_download.htm'
            }
        },

        /**
         * A map of players to the file extensions they support. Each member of
         * this object is the name of a player (with one exception), whose value
         * is an array of file extensions that player will "play". The one
         * exception to this rule is the "qtwmp" member, which contains extensions
         * that may be played using either QuickTime or Windows Media Player.
         *
         * - img: Image file extensions
         * - swf: Flash SWF file extensions
         * - flv: Flash video file extensions (will be played by JW FLV player)
         * - qt: Movie file extensions supported by QuickTime
         * - wmp: Movie file extensions supported by Windows Media Player
         * - qtwmp: Movie file extensions supported by both QuickTime and Windows Media Player
         * - iframe: File extensions that will be display in an iframe
         *
         * IMPORTANT: If this object is to be modified, it must be copied in its
         * entirety and tweaked because it is not merged recursively with the
         * default. Also, any modifications must be passed into Shadowbox.init
         * for speed reasons.
         */
        ext: {
            img:        ['png', 'jpg', 'jpeg', 'gif', 'bmp'],
            swf:        ['swf'],
            flv:        ['flv'],
            qt:         ['dv', 'mov', 'moov', 'movie', 'mp4'],
            wmp:        ['asf', 'wm', 'wmv'],
            qtwmp:      ['avi', 'mpg', 'mpeg'],
            iframe:     ['asp', 'aspx', 'cgi', 'cfm', 'htm', 'html', 'jsp', 'pl', 'php', 'php3', 'php4', 'php5', 'phtml', 'rb', 'rhtml', 'shtml', 'txt', 'vbs']
        },

        fadeDuration: 0.35,         // Duration of the fading animations (seconds)
        flashBgColor: '#000000',    // Default background color for Flash
        flvPlayer: 'flvplayer.swf', // Path to flvplayer.swf
        handleException: null,      // Callback to handle exceptions

        /**
         * How to handle content that is too large to display in its entirety
         * (and is resizable). A value of 'resize' will resize the content while
         * preserving aspect ratio and display it at the smaller resolution. If
         * the content is an image, a value of 'drag' will display the image at
         * its original resolution but it will be draggable within Shadowbox. A
         * value of 'none' will display the content at its original resolution
         * but it may be cropped.
         */
        handleOversize: 'resize',

        /**
         * The mode to use when handling unsupported media. May be either
         * 'remove' or 'link'. If it is 'remove', the unsupported gallery item
         * will merely be removed from the gallery. If it is the only item in
         * the gallery, the link will simply be followed. If it is 'link', a
         * link will be provided to the appropriate plugin page in place of the
         * gallery element.
         */
        handleUnsupported: 'link',

        initialHeight: 160,         // Initial height (pixels)
        initialWidth: 320,          // Initial width (pixels)
        modal: false,               // Trigger Shadowbox.close() when overlay is
                                    // clicked
        onChange: null,             // Hook function to be fired when changing
                                    // from one item to the next. Is passed the
                                    // item that is about to be displayed
        onClose: null,              // Hook function to be fired when closing.
                                    // Is passed the most recent item
        onFinish: null,             // Hook function to be fired when finished
                                    // loading content. Is passed current
                                    // gallery item
        onOpen: null,               // Hook function to be fired when opening.
                                    // Is passed the current gallery item
        overlayColor: '#000',       // Color to use for modal overlay
        overlayOpacity: 0.8,        // Opacity to use for modal overlay
        resizeDuration: 0.35,       // Duration of resizing animations (seconds)
        showOverlay: true,          // Show the overlay
        showMovieControls: true,    // Enable movie controls on movie players
        skipSetup: false,           // Skip calling Shadowbox.setup() during
                                    // Shadowbox.init()
        slideshowDelay: 0,          // Delay to use for slideshows (seconds). If
                                    // set to any duration other than 0, is interval
                                    // at which slideshow will advance
        viewportPadding: 20         // Amount of padding to maintain around the
                                    // edge of the viewport at all times (pixels)
    };

    // shorthand
    var SB = Shadowbox;
    var SL = SB.lib;

    /**
     * Stores the default set of options in case a custom set of options is used
     * on a link-by-link basis so we can restore them later.
     *
     * @var         Object
     * @private
     */
    var default_options;

    /**
     * An object containing some regular expressions we'll need later. Compiled
     * up front for speed.
     *
     * @var         Object
     * @private
     */
    var RE = {
        alpha:          /alpha\([^\)]*\)/gi,            // IE element alpha filter
        domain:         /:\/\/(.*?)[:\/]/,              // domain prefix
        inline:         /#(.+)$/,                       // inline element id
        rel:            /^(light|shadow)box/i,          // rel attribute format
        gallery:        /^(light|shadow)box\[(.*?)\]/i, // rel attribute format for gallery link
        unsupported:    /^unsupported-(\w+)/,           // unsupported media type
        param:          /\s*([a-z_]*?)\s*=\s*(.+)\s*/,  // rel string parameter
        empty:          /^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i // elements that don't have children
    };

    /**
     * A cache of options for links that have been set up for use with
     * Shadowbox.
     *
     * @var         Array
     * @private
     */
    var cache = [];

    /**
     * An array containing the gallery objects currently being viewed. In the
     * case of non-gallery items, this will only hold one object.
     *
     * @var         Array
     * @private
     */
    var gallery;

    /**
     * The array index of the current gallery that is currently being viewed.
     *
     * @var         Number
     * @private
     */
    var current;

    /**
     * The current content object.
     *
     * @var         Object
     * @private
     */
    var content;

    /**
     * The id to use for content objects.
     *
     * @var         String
     * @private
     */
    var content_id = 'sb-content';

    /**
     * Holds the current dimensions of Shadowbox as calculated by
     * setDimensions(). Contains the following properties:
     *
     * - height: The total height of #sb-wrapper (including title & info bars)
     * - width: The total width of #sb-wrapper
     * - inner_h: The height of #sb-body
     * - inner_w: The width of #sb-body
     * - top: The top to use for #sb-wrapper
     * - left: The left to use for #sb-wrapper
     * - resize_h: The height to use for resizable content
     * - resize_w: The width to use for resizable content
     * - drag: True if dragging should be enabled (oversized image)
     *
     * @var         Object
     * @private
     */
    var dims;

    /**
     * Keeps track of whether or not Shadowbox has been initialized. We never
     * want to initialize twice.
     *
     * @var         Boolean
     * @private
     */
    var initialized = false;

    /**
     * Keeps track of whether or not Shadowbox is activated.
     *
     * @var         Boolean
     * @private
     */
    var activated = false;

    /**
     * Keeps track of whether or not the overlay is activated.
     *
     * @var     Boolean
     * @private
     */
    var overlay_on = false;

    /**
     * The timeout id for the slideshow transition function.
     *
     * @var         Number
     * @private
     */
    var slide_timer;

    /**
     * Keeps track of the time at which the current slideshow frame was
     * displayed.
     *
     * @var         Number
     * @private
     */
    var slide_start;

    /**
     * The delay on which the next slide will display.
     *
     * @var         Number
     * @private
     */
    var slide_delay = 0;

    /**
     * A cache for elements that are troublesome for modal overlays.
     *
     * @var         Array
     * @private
     */
    var v_cache = [];

    /**
     * Simple browser detection.
     *
     * @var         Object
     * @private
     */
    var ua = navigator.userAgent.toLowerCase();
    var client = {
        isIE:       ua.indexOf('msie') > -1,
        isIE7:      ua.indexOf('msie 7') > -1,
        isGecko:    ua.indexOf('gecko') > -1 && ua.indexOf('safari') == -1,
        isWindows:  ua.indexOf('windows') > -1 || ua.indexOf('win32') > -1,
        isMac:      ua.indexOf('macintosh') > -1 || ua.indexOf('mac os x') > -1,
        isLinux:    ua.indexOf('linux') > -1
    };

    /**
     * You're not still using IE6 are you?
     *
     * @var         Boolean
     * @private
     */
    var ltIE7 = client.isIE && !client.isIE7;

    /**
     * Applies all properties of e to o.
     *
     * @param   Object      o       The original object
     * @param   Object      e       The extension object
     * @return  Object              The original object with all properties
     *                              of the extension object applied
     * @private
     */
    var apply = function(o, e){
        for(var p in e) o[p] = e[p];
        return o;
    };

    apply(Shadowbox.lib, {

        /**
         * Calls the given function for each element of obj. The obj element must
         * be array-like. If scope is not explicitly given, the callback will be
         * called with a scope of the current item.
         *
         * @param   mixed       obj     The object containing the elements
         * @param   Function    fn      The callback function
         * @param   mixed       scope   The scope of the callback
         * @return  void
         * @private
         */
        each: function(obj, fn, scope){
            for(var i = 0, len = obj.length; i < len; ++i){
                fn.call(scope || obj[i], obj[i], i, obj);
            }
        },

        /**
         * Creates an HTML string from an object representing HTML elements. Based
         * on Ext.DomHelper's createHtml.
         *
         * @param   Object      obj     The HTML definition object
         * @return  String              An HTML string
         * @public
         * @static
         */
        createHTML: function(obj){
            var html = '<' + obj.tag;
            for(var attr in obj){
                if(attr == 'tag' || attr == 'html' || attr == 'children') continue;
                html += (attr == 'cls') ? ' class="' + obj['cls'] + '"' : ' ' + attr + '="' + obj[attr] + '"';
            }
            if(RE.empty.test(obj.tag)){
                html += '/>';
            }else{
                html += '>';
                if(obj.children) SL.each(obj.children, function(c){ html += this.createHTML(c); }, this);
                if(obj.html) html += obj.html;
                html += '</' + obj.tag + '>';
            }
            return html;
        },

        /**
         * Sets the opacity of the given element to the specified level.
         *
         * @param   HTMLElement     el          The element
         * @param   Number          opacity     The opacity to use
         * @return  void
         * @public
         * @static
         */
        setOpacity: function(el, opacity){
            var s = el.style;
            if(window.ActiveXObject){ // IE
                s.zoom = 1; // give "layout"
                s.filter = (s.filter || '').replace(RE.alpha, '') +
                    (opacity == 1 ? '' : ' alpha(opacity=' + (opacity * 100) + ')');
            }else{
                s.opacity = opacity;
            }
        }

    });

    /**
     * Contains plugin support information. Each property of this object is a
     * boolean indicating whether that plugin is supported.
     *
     * - fla: Flash player
     * - qt: QuickTime player
     * - wmp: Windows Media player
     * - f4m: Flip4Mac plugin
     *
     * @var         Object
     * @private
     */
    var plugins;

    // detect plugin support
    if(navigator.plugins && navigator.plugins.length){
        var names = [];
        SL.each(navigator.plugins, function(p){ names.push(p.name); });
        names = names.join();
        var detectPlugin = function(n){
            return names.indexOf(n) > -1;
        };
        var f4m = detectPlugin('Flip4Mac');
        plugins = {
            fla:    detectPlugin('Shockwave Flash'),
            qt:     detectPlugin('QuickTime'),
            wmp:    !f4m && detectPlugin('Windows Media'), // if it's Flip4Mac, it's not really WMP
            f4m:    f4m
        };
    }else{
        var detectPlugin = function(n){
            try{ var axo = new ActiveXObject(n); }catch(e){}
            return !!axo;
        };
        plugins = {
            fla:    detectPlugin('ShockwaveFlash.ShockwaveFlash'),
            qt:     detectPlugin('QuickTime.QuickTime'),
            wmp:    detectPlugin('wmplayer.ocx'),
            f4m:    false
        };
    }

    /**
     * Determines if the given object is an anchor/area element.
     *
     * @param   mixed       el      The object to check
     * @return  Boolean             True if the object is a link element
     * @private
     */
    var isLink = function(el){
        return el && typeof el.tagName == 'string' && (el.tagName.toUpperCase() == 'A' || el.tagName.toUpperCase() == 'AREA');
    };

    /**
     * Animates any numeric (not color) style of the given element from its
     * current state to the given value. Defaults to using pixel-based
     * measurements.
     *
     * @param   HTMLElement     el      The DOM element to animate
     * @param   String          p       The property to animate (in camelCase)
     * @param   mixed           to      The value to animate to
     * @param   Number          d       The duration of the animation (in
     *                                  seconds)
     * @param   Function        cb      A callback function to call when the
     *                                  animation completes
     * @return  void
     * @private
     * @static
     */
    var animate = function(el, p, to, d, cb){
        var from = parseFloat(SL.getStyle(el, p));
        if(isNaN(from)) from = 0;

        var delta = to - from;
        if(delta == 0){
            if(cb) cb();
            return; // nothing to animate
        }

        var op = p == 'opacity',
            effect = function(ease){
                var to = from + ease * delta;
                if(op){
                    SL.setOpacity(el, to);
                }else{
                    el.style[p] = to + 'px'; // default unit is px
                }
            };

        // cancel the animation here if duration is 0 or if set in the options
        if(!d || (!options.animate && !op) || (op && !options.animateFade)){
            effect(1);
            if(cb) cb();
            return;
        }

        d *= 1000; // convert to milliseconds
        var begin = new Date().getTime(),
            end = begin + d,
            timer = setInterval(function(){
                var time = new Date().getTime();
                if(time >= end){ // end of animation
                    clearInterval(timer);
                    effect(1);
                    if(cb) cb();
                }else{
                    effect(options.ease((time - begin) / d));
                }
            }, 10); // 10 ms interval is minimum on webkit
    };

    /**
     * A utility function used by the fade functions to clear the opacity
     * style setting of the given element. Required in some cases for IE.
     *
     * @param   HTMLElement     el      The DOM element
     * @return  void
     * @private
     */
    var clearOpacity = function(el){
        var s = el.style;
        if(client.isIE){
            if(typeof s.filter == 'string' && (/alpha/i).test(s.filter)){
                // careful not to overwrite other filters!
                s.filter = s.filter.replace(/[\w\.]*alpha\(.*?\);?/i, '');
            }
        }else{
            s.opacity = '';
        }
    };

    /**
     * Determines the player needed to display the file at the given URL. If
     * the file type is not supported, the return value will be 'unsupported'.
     * If the file type is not supported but the correct player can be
     * determined, the return value will be 'unsupported-*' where * will be the
     * player abbreviation (e.g. 'qt' = QuickTime).
     *
     * @param   String          url     The url of the file
     * @return  String                  The name of the player to use
     * @private
     */
    var getPlayer = function(url){
        var m = url.match(RE.domain),
            d = m && document.domain == m[1]; // same domain
        if(url.indexOf('#') > -1 && d) return 'inline';
        var q = url.indexOf('?');
        if(q > -1) url = url.substring(0, q); // strip query string for player detection purposes
        if(RE.img.test(url)) return 'img';
        if(RE.swf.test(url)) return plugins.fla ? 'swf' : 'unsupported-swf';
        if(RE.flv.test(url)) return plugins.fla ? 'flv' : 'unsupported-flv';
        if(RE.qt.test(url)) return plugins.qt ? 'qt' : 'unsupported-qt';
        if(RE.wmp.test(url)){
            if(plugins.wmp) return 'wmp';
            if(plugins.f4m) return 'qt';
            if(client.isMac) return plugins.qt ? 'unsupported-f4m' : 'unsupported-qtf4m';
            return 'unsupported-wmp';
        }else if(RE.qtwmp.test(url)){
            if(plugins.qt) return 'qt';
            if(plugins.wmp) return 'wmp';
            return client.isMac ? 'unsupported-qt' : 'unsupported-qtwmp';
        }else if(!d || RE.iframe.test(url)){
            return 'iframe';
        }
        return 'unsupported'; // same domain, not supported
    };

    /**
     * Handles all clicks on links that have been set up to work with Shadowbox
     * and cancels the default event behavior when appropriate.
     *
     * @param   {Event}         ev          The click event object
     * @return  void
     * @private
     */
    var handleClick = function(ev){
        // get anchor/area element
        var link;
        if(isLink(this)){
            link = this; // jQuery, Prototype, YUI
        }else{
            link = SL.getTarget(ev); // Ext, standalone
            while(!isLink(link) && link.parentNode){
                link = link.parentNode;
            }
        }

        //SL.preventDefault(ev); // good for debugging

        if(link){
            SB.open(link);
            if(gallery.length) SL.preventDefault(ev); // stop event
        }
    };

    /**
     * Toggles the display of the nav control with the given id on and off.
     *
     * @param   String      id      The id of the navigation control
     * @param   Boolean     on      True to toggle on, false to toggle off
     * @return  void
     * @private
     */
    var toggleNav = function(id, on){
        var el = SL.get('sb-nav-' + id);
        if(el) el.style.display = on ? '' : 'none';
    };

    /**
     * Builds the content for the title and information bars.
     *
     * @param   Function    cb      A callback function to execute after the
     *                              bars are built
     * @return  void
     * @private
     */
    var buildBars = function(cb){
        var obj = gallery[current],
            nav = SL.get('sb-nav'),
            counter = SL.get('sb-counter');

        // build the title, if present
        SL.get('sb-title-inner').innerHTML = obj.title || '';

        // build the nav
        if(nav){
            var c, n, pl, pa, p;

            // need to build the nav?
            if(options.displayNav){
                c = true;
                // next & previous links
                var len = gallery.length;
                if(len > 1){
                    if(options.continuous){
                        n = p = true; // show both
                    }else{
                        n = (len - 1) > current; // not last in gallery, show next
                        p = current > 0; // not first in gallery, show previous
                    }
                }
                // in a slideshow?
                if(options.slideshowDelay > 0 && hasNext()){
                    pa = slide_timer != 'paused';
                    pl = !pa;
                }
            }else{
                c = n = pl = pa = p = false;
            }

            toggleNav('close', c);
            toggleNav('next', n);
            toggleNav('play', pl);
            toggleNav('pause', pa);
            toggleNav('previous', p);
        }

        // build the counter
        if(counter){
            var co = '';

            // need to build the counter?
            if(options.displayCounter && gallery.length > 1){
                if(options.counterType == 'skip'){
                    // limit the counter?
                    var i = 0, len = gallery.length, end = len;
                    var limit = parseInt(options.counterLimit);
                    if(limit < len && limit > 2){ // support large galleries
                        var h = Math.floor(limit / 2);
                        i = current - h;
                        if(options.endlessPaging == 'true'){
                           if(i < 0) i += len;
                        }else{
                           if(i < 0) i = 0;
                        }
                        end = current + (limit - h);
                        if(options.endlessPaging == 'true'){
                           if(end > len) end -= len;
                        }else{
                           if(end > len) end = len;
                        }
                    }
                    while(i != end){
                        if(i == len) i = 0;
                        co += '<a onclick="Shadowbox.change(' + i + ');"';
                        if(i == current) co += ' class="sb-counter-current"';
                        co += '>' + (++i) + '</a>';
                    }
                }else{ // default
                    co = (current + 1) + ' ' + SB.LANG.of + ' ' + len;
                }
            }

            counter.innerHTML = co;
        }

        cb();
    };

    /**
     * Hides the title and info bars.
     *
     * @param   Boolean     anim    True to animate the transition
     * @param   Function    cb      A callback function to execute after the
     *                              animation completes
     * @return  void
     * @private
     */
    var hideBars = function(anim, cb){
        var sw = SL.get('sb-wrapper'),
            st = SL.get('sb-title'),
            si = SL.get('sb-info'),
            ti = SL.get('sb-title-inner'),
            ii = SL.get('sb-info-inner'),
            t = parseInt(SL.getStyle(ti, 'height')) || 0,
            b = parseInt(SL.getStyle(ii, 'height')) || 0;

        var fn = function(){
            // hide bars here in case of overflow, build after hidden
            ti.style.visibility = ii.style.visibility = 'hidden';
            buildBars(cb);
        };

        if(anim){
            animate(st, 'height', 0, 0.35);
            animate(si, 'height', 0, 0.35);
            animate(sw, 'paddingTop', t, 0.35);
            animate(sw, 'paddingBottom', b, 0.35, fn);
        }else{
            st.style.height = si.style.height = '0px';
            sw.style.paddingTop = t + 'px';
            sw.style.paddingBottom = b + 'px';
            fn();
        }
    };

    /**
     * Shows the title and info bars.
     *
     * @param   Function    cb      A callback function to execute after the
     *                              animation completes
     * @return  void
     * @private
     */
    var showBars = function(cb){
        var sw = SL.get('sb-wrapper'),
            st = SL.get('sb-title'),
            si = SL.get('sb-info'),
            ti = SL.get('sb-title-inner'),
            ii = SL.get('sb-info-inner'),
            t = parseInt(SL.getStyle(ti, 'height')) || 0,
            b = parseInt(SL.getStyle(ii, 'height')) || 0;

        // clear visibility before animating into view
        ti.style.visibility = ii.style.visibility = '';

        // show title?
        if(ti.innerHTML != ''){
            animate(st, 'height', t, 0.35);
            animate(sw, 'paddingTop', 0, 0.35);
        }
        animate(si, 'height', b, 0.35);
        animate(sw, 'paddingBottom', 0, 0.35, cb);
    };

    /**
     * Loads the Shadowbox with the current piece.
     *
     * @return  void
     * @private
     */
    var loadContent = function(){
        var obj = gallery[current];
        if(!obj) return; // invalid

        var changing = false;
        if(content){
            content.remove(); // remove old content first
            changing = true; // changing from some previous content
        }

        // determine player, inline is really just HTML
        var p = obj.player == 'inline' ? 'html' : obj.player;

        // make sure player is loaded
        if(typeof SB[p] != 'function'){
            SB.raise('Unknown player ' + obj.player);
        }
        content = new SB[p](content_id, obj); // instantiate new content object

        listenKeys(false); // disable the keyboard temporarily
        toggleLoading(true);

        hideBars(changing, function(){ // if changing, animate the bars transition
            if(!content) return;

            // if opening, clear #sb-wrapper display
            if(!changing) SL.get('sb-wrapper').style.display = '';

            var fn = function(){
                resizeContent(function(){
                    if(!content) return;

                    showBars(function(){
                        if(!content) return;

                        // append content just before hiding the loading layer
                        SL.get('sb-body-inner').innerHTML = SL.createHTML(content.markup(dims));

                        toggleLoading(false, function(){
                            if(!content) return;

                            // call onLoad callback if present, fire onFinish handler
                            if(content.onLoad) content.onLoad();
                            if(options.onFinish && typeof options.onFinish == 'function') options.onFinish(gallery[current]);
                            if(slide_timer != 'paused') SB.play(); // kick off next slide
                            listenKeys(true); // re-enable the keyboard
                        });
                    });
                });
            };

            if(typeof content.ready != 'undefined'){ // does the object have a ready property?
                var id = setInterval(function(){ // if so, wait for the object to be ready
                    if(content){
                        if(content.ready){
                            clearInterval(id); // clean up
                            id = null;
                            fn();
                        }
                    }else{ // content has been removed
                        clearInterval(id);
                        id = null;
                    }
                }, 100);
            }else{
                fn();
            }
        });

        // preload neighboring gallery images
        if(gallery.length > 1){
            var next = gallery[current + 1] || gallery[0];
            if(next.player == 'img'){
                var a = new Image();
                a.src = next.content;
            }
            var prev = gallery[current - 1] || gallery[gallery.length - 1];
            if(prev.player == 'img'){
                var b = new Image();
                b.src = prev.content;
            }
        }
    };

    /**
     * Calculates the dimensions for Shadowbox, taking into account the borders
     * and surrounding elements of #sb-body. If the height/width
     * combination is too large for Shadowbox, the handleOversize option is set
     * to 'resize', and the content is resizable, the resized dimensions will be
     * returned (preserving the original aspect ratio). Otherwise, the
     * originally calculated dimensions will be used. Stores all dimensions in
     * the private dims variable.
     *
     * @param   Number      height      The content player height
     * @param   Number      width       The content player width
     * @param   Boolean     resizable   True if the content is able to be
     *                                  resized. Defaults to false.
     * @return  void
     * @private
     */
    var setDimensions = function(height, width, resizable){
        resizable = resizable || false;

        var sb = SL.get('sb-body'),
            sbi = SL.get('sb-body-inner')
            sw = SL.get('sb-wrapper'),
            so = SL.get('sb-overlay'),
            h = height = parseInt(height),
            w = width = parseInt(width),
            view_h = so.offsetHeight, // measure overlay for IE6's sake
            view_w = so.offsetWidth,
            pad = options.viewportPadding;

        // calculate the max width
        var lr = sw.offsetWidth - sbi.offsetWidth,
            extra_x = 2 * pad + lr;
        if(w + extra_x >= view_w){
            w = view_w - extra_x;
        }

        // calculate the max height
        var tb = sw.offsetHeight - sbi.offsetHeight,
            extra_y = 2 * pad + tb;
        if(h + extra_y >= view_h){
            h = view_h - extra_y;
        }

        // handle oversized content
        var drag = false,
            resize_h = height,
            resize_w = width,
            handle = options.handleOversize;
        if(resizable && (handle == 'resize' || handle == 'drag')){
            var change_h = (height - h) / height,
                change_w = (width - w) / width;
            if(handle == 'resize'){
                if(change_h > change_w){
                    w = Math.round((width / height) * h);
                }else if(change_w > change_h){
                    h = Math.round((height / width) * w);
                }
                // adjust resized height or width accordingly
                resize_w = w;
                resize_h = h;
            }else{
                // drag on oversized images only
                var link = gallery[current];
                if(link) drag = link.player == 'img' && (change_h > 0 || change_w > 0);
            }
        }

        // update dims
        dims = {
            height:     h + tb,
            width:      w + lr,
            inner_h:    h,
            inner_w:    w,
            top:        (view_h - (h + extra_y)) / 2 + pad,
            left:       (view_w - (w + extra_x)) / 2 + pad,
            resize_h:   resize_h,
            resize_w:   resize_w,
            drag:       drag
        };
    };

    /**
     * Resizes Shadowbox to the given height and width. If the callback
     * parameter is given, the transition will be animated and the callback
     * function will be called when the animation completes. Note: The private
     * content variable must be updated before calling this function.
     *
     * @param   Function    cb      A callback function to execute after the
     *                              content has been resized
     * @return  void
     * @private
     */
    var resizeContent = function(cb){
        if(!content) return; // no content

        // set new dimensions
        setDimensions(content.height, content.width, content.resizable);

        if(typeof cb == 'function'){
            switch(options.animSequence){
                case 'hw':
                    adjustHeight(dims.inner_h, dims.top, true, function(){
                        adjustWidth(dims.width, dims.left, true, cb);
                    });
                    break;
                case 'wh':
                    adjustWidth(dims.width, dims.left, true, function(){
                        adjustHeight(dims.inner_h, dims.top, true, cb);
                    });
                    break;
                case 'sync':
                default:
                    adjustWidth(dims.width, dims.left, true);
                    adjustHeight(dims.inner_h, dims.top, true, cb);
            }
        }else{ // window resize
            adjustWidth(dims.width, dims.left, false);
            adjustHeight(dims.inner_h, dims.top, false);
            var c = SL.get(content_id);
            if(c){
                // resize resizable content when in resize mode
                if(content.resizable && options.handleOversize == 'resize'){
                    c.height = dims.resize_h;
                    c.width = dims.resize_w;
                }
                // fix draggable positioning if enlarging viewport
                if(gallery[current].player == 'img' && options.handleOversize == 'drag'){
                    var top = parseInt(SL.getStyle(c, 'top'));
                    if(top + content.height < dims.inner_h){
                        c.style.top = dims.inner_h - content.height + 'px';
                    }
                    var left = parseInt(SL.getStyle(c, 'left'));
                    if(left + content.width < dims.inner_w){
                        c.style.left = dims.inner_w - content.width + 'px';
                    }
                }
            }
        }
    };

    /**
     * Adjusts the height of #sb-body and centers #sb-wrapper vertically
     * in the viewport.
     *
     * @param   Number      height      The height to use for #sb-body
     * @param   Number      top         The top to use for #sb-wrapper
     * @param   Boolean     anim        True to animate the transition
     * @param   Function    cb          A callback to use when the animation
     *                                  completes
     * @return  void
     * @private
     */
    var adjustHeight = function(height, top, anim, cb){
        var sb = SL.get('sb-body'),
            s = SL.get('sb-wrapper'),
            height = parseInt(height),
            top = parseInt(top);

        if(anim){
            animate(sb, 'height', height, options.resizeDuration);
            animate(s, 'top', top, options.resizeDuration, cb);
        }else{
            sb.style.height = height + 'px';
            s.style.top = top + 'px';
            if(cb) cb();
        }
    };

    /**
     * Adjusts the width and left of #sb-wrapper.
     *
     * @param   Number      width       The width to use for #sb-wrapper
     * @param   Number      left        The left to use for #sb-wrapper
     * @param   Boolean     anim        True to animate the transition
     * @param   Function    cb          A callback to use when the animation
     *                                  completes
     * @return  void
     * @private
     */
    var adjustWidth = function(width, left, anim, cb){
        var s = SL.get('sb-wrapper'),
            width = parseInt(width),
            left = parseInt(left);

        if(anim){
            animate(s, 'width', width, options.resizeDuration);
            animate(s, 'left', left, options.resizeDuration, cb);
        }else{
            s.style.width = width + 'px';
            s.style.left = left + 'px';
            if(cb) cb();
        }
    };

    /**
     * Sets up a listener on the document for keystrokes.
     *
     * @param   Boolean     on      True to enable the listener, false to turn
     *                              it off
     * @return  void
     * @private
     */
    var listenKeys = function(on){
        if(!options.enableKeys) return;
        SL[(on ? 'add' : 'remove') + 'Event'](document, 'keydown', handleKey);
    };

    /**
     * A listener function that is fired when a key is pressed.
     *
     * @param   mixed       e       The event object
     * @return  void
     * @private
     */
    var handleKey = function(e){
        var code = SL.keyCode(e);

        // attempt to prevent default key action
        SL.preventDefault(e);

        if(code == 81 || code == 88 || code == 27){ // q, x, or esc
            SB.close();
        }else if(code == 37){ // left arrow
            SB.previous();
        }else if(code == 39){ // right arrow
            SB.next();
        }else if(code == 32){ // space bar
            SB[(typeof slide_timer == 'number' ? 'pause' : 'play')]();
        }
    };

    /**
     * Toggles the visibility of the "loading" layer.
     *
     * @param   Boolean     on      True to toggle on, false to toggle off
     * @param   Function    cb      The callback function to call when toggling
     *                              completes
     * @return  void
     * @private
     */
    var toggleLoading = function(on, cb){
        var ld = SL.get('sb-loading');
        if(!ld){
            if(cb) cb();
            return;
        }

        if(on){
            ld.style.display = '';
            if(cb) cb();
        }else{
            var p = gallery[current].player;
            var anim = (p == 'img' || p == 'html'); // fade on images & html
            var fn = function(){
                ld.style.display = 'none';
                clearOpacity(ld);
                if(cb) cb();
            };
            if(anim){
                animate(ld, 'opacity', 0, options.fadeDuration, fn);
            }else{
                fn();
            }
        }
    };

    /**
     * Sets the top of the container element. This is only necessary in IE6
     * where the container uses absolute positioning instead of fixed.
     *
     * @return  void
     * @private
     */
    var fixTop = function(){
        SL.get('sb-container').style.top = document.documentElement.scrollTop + 'px';
    };

    /**
     * Determines if there is a next piece to display in the current gallery.
     *
     * @return  bool            True if there is another piece, false otherwise
     * @private
     */
    var hasNext = function(){
        return gallery.length > 1 && (current != gallery.length - 1 || options.continuous);
    };

    /**
     * Toggles the visibility of #sb-container and sets its size (if on
     * IE6). Also toggles the visibility of elements (<select>, <object>, and
     * <embed>) that are troublesome for semi-transparent modal overlays. IE has
     * problems with <select> elements, while Firefox has trouble with
     * <object>s.
     *
     * @param   Function    cb      A callback to call after toggling on, absent
     *                              when toggling off
     * @return  void
     * @private
     */
    var toggleVisible = function(cb){
        var on = !(cb), v = on ? 'visible' : 'hidden';
        if(!on){
            SL.each(['select', 'object', 'embed'], function(sel){
                SL.each(document.getElementsByTagName(sel), function(el){
                    v_cache.push([el, SL.getStyle(el, 'visibility')]);
                    el.style.visibility = v;
                });
            });
        }else{
            // reset to stored visibility settings
            SL.each(v_cache, function(c){
                c[0].style.visibility = c[1];
            });
        }

        // resize & show container
        var so = SL.get('sb-overlay'),
            sc = SL.get('sb-container'),
            sb = SL.get('sb-wrapper');
        if(cb){
            if(ltIE7){
                // fix container top before showing
                fixTop();
                SL.addEvent(window, 'scroll', fixTop);
            }
            if(options.showOverlay){
                overlay_on = true;

                // set overlay color/opacity
                so.style.backgroundColor = options.overlayColor;
                SL.setOpacity(so, 0);
                if(!options.modal) SL.addEvent(so, 'click', SB.close);

                sb.style.display = 'none'; // cleared in loadContent()
            }
            sc.style.visibility = 'visible';
            if(overlay_on){
                // fade in effect
                animate(so, 'opacity', parseFloat(options.overlayOpacity), options.fadeDuration, cb);
            }else{
                cb();
            }
        }else{
            SL.removeEvent(so, 'click', SB.close);
            if(ltIE7) SL.removeEvent(window, 'scroll', fixTop);
            if(overlay_on){
                // fade out effect
                sb.style.display = 'none';
                animate(so, 'opacity', 0, options.fadeDuration, function(){
                    sc.style.visibility = 'hidden';
                    sc.style.display = sb.style.display = '';
                    clearOpacity(so);
                });
            }else{
                sc.style.visibility = 'hidden';
            }
        }
    };

    /**
     * Initializes the Shadowbox environment. Loads the skin (if necessary),
     * compiles the player matching regular expressions, and sets up the
     * window resize listener.
     *
     * @param   Object      opts    (optional) The default options to use
     * @return  void
     * @public
     * @static
     */
    Shadowbox.init = function(opts){
        // don't initialize twice
        if(initialized) return;

        // make sure language is loaded
        if(typeof SB.LANG == 'undefined'){
            SB.raise('No Shadowbox language loaded');
            return;
        }
        // make sure skin is loaded
        if(typeof SB.SKIN == 'undefined'){
            SB.raise('No Shadowbox skin loaded');
            return;
        }

        // apply skin & init options
        if(SB.SKIN.options) apply(options, SB.SKIN.options);
        apply(options, opts || {});

        // append markup to body
        var markup = SB.SKIN.markup.replace(/\{(\w+)\}/g, function(m, p){
            return SB.LANG[p];
        });
        SL.append(document.body || document.documentElement, markup);

        // several fixes for IE6
        if(ltIE7){
            // give sb-body "layout"...whatever that is
            SL.get('sb-body').style.zoom = 1;
            // support transparent PNG's via AlphaImageLoader
            var png = SB.SKIN.png;
            if(png && png.constructor == Array){
                var el, m;
                SL.each(png, function(id){
                    el = SL.get(id);
                    if(el){
                        m = SL.getStyle(el, 'backgroundImage').match(/url\("(.*\.png)"\)/);
                        if(m){
                            el.style.backgroundImage = 'none';
                            el.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true,src=' + m[1] + ',sizingMethod=scale);';
                        }
                    }
                });
            }
        }

        // compile file type regular expressions here for speed
        for(var e in options.ext){
            RE[e] = new RegExp('\.(' + options.ext[e].join('|') + ')\s*$', 'i');
        }

        // set up window resize event handler
        var id;
        SL.addEvent(window, 'resize', function(){
            // use 50 ms event buffering to prevent jerky window resizing
            if(id){
                clearTimeout(id);
                id = null;
            }
            // check if activated because IE7 fires window resize event when
            // #sb-container display is set to block
            if(activated) id = setTimeout(resizeContent, 50);
        });

        if(!options.skipSetup) SB.setup();
        initialized = true;
    };

    /**
     * Dynamically includes the given script in the page.
     *
     * @param   String  script      The name of the script to include
     * @return  void
     * @private
     * @static
     */
    var include = function(script){
        // Safari 2.0 fails using DOM, use document.write instead
        document.write('<scr' + 'ipt type="text/javascript" src="' + script + '"><\/script>');
    };

    /**
     * Dynamically loads the specified skin for use with Shadowbox. If the skin
     * is included already in the page via the appropriate <script> and <link>
     * tags, this function does not need to be called. Otherwise, this function
     * must be called before window.onload.
     *
     * @param   String      skin        The directory where the skin is located
     * @param   String      dir         The directory where the Shadowbox skin
     *                                  files are located
     * @return  void
     * @public
     * @static
     */
    Shadowbox.loadSkin = function(skin, dir){
        if(!(/\/$/.test(dir))) dir += '/';
        skin = dir + skin + '/';
        document.write('<link rel="stylesheet" type="text/css" href="' + skin + 'skin.css">');
        include(skin + 'skin.js');
    };

    /**
     * Dynamically loads the specified language file to be used with Shadowbox.
     * If the language file is included already in the page via the appropriate
     * <script> tag, this function does not need to be called. Otherwise, this
     * function must be called before window.onload.
     *
     * @param   String      lang        The language abbreviation (e.g. en)
     * @param   String      dir         The directory where the Shadowbox
     *                                  language file(s) is located
     * @return  void
     * @public
     * @static
     */
    Shadowbox.loadLanguage = function(lang, dir){
        if(!(/\/$/.test(dir))) dir += '/';
        include(dir + 'shadowbox-' + lang + '.js');
    };

    /**
     * Dynamically loads the specified player(s) to be used with Shadowbox. If
     * the needed player(s) is already included in the page via the appropriate
     * <script> tag(s), this function does not need to be called. Otherwise,
     * this function must be called before window.onload.
     *
     * @param   Array       players     The player(s) to load
     * @param   String      dir         The director where the Shadowbox player
     *                                  file(s) is located
     * @return  void
     * @public
     * @static
     */
    Shadowbox.loadPlayer = function(players, dir){
        if(typeof players == 'string') players = [players];
        if(!(/\/$/.test(dir))) dir += '/';
        SL.each(players, function(p){
            include(dir + 'shadowbox-' + p + '.js');
        });
    };

    /**
     * Sets up listeners on the given links that will trigger Shadowbox. If no
     * links are given, this method will set up every anchor element on the page
     * with the appropriate rel attribute. Note: Because AREA elements do not
     * support the rel attribute, they must be explicitly passed to this method.
     *
     * @param   Array       links       An array (or array-like) list of anchor
     *                                  and/or area elements to set up
     * @param   Object      opts        Some options to use for the given links
     * @return  void
     * @public
     * @static
     */
    Shadowbox.setup = function(links, opts){
        // get links if none specified
        if(!links){
            var links = [], rel;
            SL.each(document.getElementsByTagName('a'), function(a){
                rel = a.getAttribute('rel');
                if(rel && RE.rel.test(rel)) links.push(a);
            });
        }else if(!links.length){
            links = [links]; // one link
        }

        SL.each(links, function(link){
            if(typeof link.shadowboxCacheKey == 'undefined'){
                // assign cache key expando, use integer primitive to avoid memory leak in IE
                link.shadowboxCacheKey = cache.length;
                SL.addEvent(link, 'click', handleClick); // add listener
            }
            cache[link.shadowboxCacheKey] = this.buildCacheObj(link, opts);
        }, this);
    };

    /**
     * Builds an object from the original link element data to store in cache.
     * These objects contain (most of) the following keys:
     *
     * - el: the link element
     * - title: the linked file title
     * - player: the player to use for the linked file
     * - content: the linked file's URL
     * - gallery: the gallery the file belongs to (optional)
     * - height: the height of the linked file (only necessary for movies)
     * - width: the width of the linked file (only necessary for movies)
     * - options: custom options to use (optional)
     *
     * @param   HTMLElement     link    The link element to process
     * @return  Object                  An object representing the link
     * @public
     * @static
     */
    Shadowbox.buildCacheObj = function(link, opts){
        var href = link.href; // don't use getAttribute() here
        var o = {
            el:         link,
            title:      link.getAttribute('title'),
            player:     getPlayer(href),
            options:    apply({}, opts || {}), // break the reference
            content:    href
        };

        // remove link-level options from top-level options
        SL.each(['player', 'title', 'height', 'width', 'gallery'], function(opt){
            if(typeof o.options[opt] != 'undefined'){
                o[opt] = o.options[opt];
                delete o.options[opt];
            }
        });

        // HTML options always trump JavaScript options, so do these last
        var rel = link.getAttribute('rel');
        if(rel){
            // extract gallery name from shadowbox[name] format
            var match = rel.match(RE.gallery);
            if(match) o.gallery = escape(match[2]);

            // other parameters
            SL.each(rel.split(';'), function(p){
                match = p.match(RE.param);
                if(match){
                    if(match[1] == 'options'){
                        eval('apply(o.options, ' + match[2] + ')');
                    }else{
                        o[match[1]] = match[2];
                    }
                }
            });
        }

        return o;
    };

    /**
     * Applies the given set of options to those currently in use. Note: Options
     * will be reset on Shadowbox.open() so this function is only useful after
     * it has already been called (while Shadowbox is open).
     *
     * @param   Object      opts        The options to apply
     * @return  void
     * @public
     * @static
     */
    Shadowbox.applyOptions = function(opts){
        if(opts){
            // use apply here to break references
            default_options = apply({}, options); // store default options
            options = apply(options, opts); // apply options
        }
    };

    /**
     * Reverts Shadowbox' options to the last default set in use before
     * Shadowbox.applyOptions() was called.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.revertOptions = function(){
        if(default_options){
            options = default_options; // revert to default options
            default_options = null; // erase for next time
        }
    };

    /**
     * Opens the given object in Shadowbox. This object may be either an
     * anchor/area element, or an object similar to the one created by
     * Shadowbox.buildCacheObj().
     *
     * @param   mixed       obj         The object or link element that defines
     *                                  what to display
     * @return  void
     * @public
     * @static
     */
    Shadowbox.open = function(obj, opts){
        // revert options
        this.revertOptions();

        // is it a link?
        if(isLink(obj)){
            if(typeof obj.shadowboxCacheKey == 'undefined' || typeof cache[obj.shadowboxCacheKey] == 'undefined'){
                // link element that hasn't been set up before
                // create on-the-fly object
                obj = this.buildCacheObj(obj, opts);
            }else{
                // link element that has been set up before, get from cache
                obj = cache[obj.shadowboxCacheKey];
            }
        }

        // is it already a gallery?
        if(obj.constructor == Array){
            gallery = obj;
            current = 0;
        }else{
            // create a copy so it doesn't get modified later
            var copy = apply({}, obj);

            // is it part of a gallery?
            if(!obj.gallery){ // single item, no gallery
                gallery = [copy];
                current = 0;
            }else{
                current = null; // reset current
                gallery = []; // clear the current gallery
                SL.each(cache, function(c){
                    if(c.gallery && c.gallery == obj.gallery){
                        if(c.content == obj.content && c.title == obj.title){
                            current = gallery.length;
                        }
                        gallery.push(apply({}, c));
                    }
                });
                // if not found in cache, prepend to front of gallery
                if(current == null){
                    gallery.unshift(copy);
                    current = 0;
                }
            }
        }
        obj = gallery[current];

        // apply custom options
        if(obj.options || opts){
            // use apply here to break references
            this.applyOptions(apply(apply({}, obj.options || {}), opts || {}));
        }

        // filter gallery for unsupported elements
        var match, r, s, a, oe = options.errors, msg, el;
        SL.each(gallery, function(g, i){
            r = false; // remove the element?
            if(g.player == 'unsupported'){ // don't support this at all
                r = true;
            }else if(match = RE.unsupported.exec(g.player)){ // handle unsupported elements
                if(options.handleUnsupported == 'link'){
                    gallery[i].player = 'html';
                    // generate a link to the appropriate plugin download page(s)
                    switch(match[1]){
                        case 'qtwmp':
                            s = 'either';
                            a = [oe.qt.url, oe.qt.name, oe.wmp.url, oe.wmp.name];
                        break;
                        case 'qtf4m':
                            s = 'shared';
                            a = [oe.qt.url, oe.qt.name, oe.f4m.url, oe.f4m.name];
                        break;
                        default:
                            s = 'single';
                            if(match[1] == 'swf' || match[1] == 'flv') match[1] = 'fla';
                            a = [oe[match[1]].url, oe[match[1]].name];
                    }
                    msg = SB.LANG.errors[s].replace(/\{(\d+)\}/g, function(m, i){
                        return a[i];
                    });
                    gallery[i].content = '<div class="sb-message">' + msg + '</div>';
                }else{
                    r = true;
                }
            }else if(g.player == 'inline'){ // handle inline elements
                // retrieve the innerHTML of the inline element
                match = RE.inline.exec(g.content);
                if(match){
                    if(el = SL.get(match[1])){
                        gallery[i].content = el.innerHTML;
                    }else{
                        SB.raise('Cannot find element with id ' + match[1]);
                    }
                }else{
                    SB.raise('Cannot find element id for inline content');
                }
            }
            if(r){
                gallery.splice(i, 1); // remove the element from the gallery
                if(i < current){
                    --current;
                }else if(i == current){
                    // if current is unsupported, look for supported neighbor
                    current = i > 0 ? current - 1 : i;
                }
                --i; // decrement to account for splice
                len = gallery.length; // gallery length has changed!
            }
        });

        // anything left to display?
        if(gallery.length){
            // fire onOpen hook
            if(options.onOpen && typeof options.onOpen == 'function') options.onOpen(obj);

            if(!activated){
                SL.get('sb-container').style.display = 'block';

                // set initial dimensions & load
                var h = options.autoDimensions && 'height' in obj ? obj.height : options.initialHeight;
                var w = options.autoDimensions && 'width' in obj ? obj.width : options.initialWidth;

                // custom skin rendering
                if(typeof SB.SKIN.render == 'function') SB.SKIN.render(h, w);

                setDimensions(h, w);
                adjustHeight(dims.inner_h, dims.top, false);
                adjustWidth(dims.width, dims.left, false);
                toggleVisible(loadContent);
            } else {
                loadContent();
            }

            activated = true;
        }
    };

    /**
     * Jumps to the piece in the current gallery with index num.
     *
     * @param   Number      num     The gallery index to view
     * @return  void
     * @public
     * @static
     */
    Shadowbox.change = function(num){
        if(!gallery) return; // no current gallery
        if(!gallery[num]){ // index does not exist
            if(!options.continuous){
                return;
            }else{
                num = num < 0 ? (gallery.length - 1) : 0; // loop
            }
        }

        if(typeof slide_timer == 'number'){
            clearTimeout(slide_timer);
            slide_timer = null;
            slide_delay = slide_start = 0; // reset slideshow variables
        }
        current = num; // update current

        // fire onChange hook
        if(options.onChange && typeof options.onChange == 'function') options.onChange(gallery[current]);
        loadContent();
    };

    /**
     * Jumps to the next piece in the gallery.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.next = function(){
        this.change(current + 1);
    };

    /**
     * Jumps to the previous piece in the gallery.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.previous = function(){
        this.change(current - 1);
    };

    /**
     * Sets the timer for the next image in the slideshow to be displayed.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.play = function(){
        if(!hasNext()) return;
        if(!slide_delay) slide_delay = options.slideshowDelay * 1000;
        if(slide_delay){
            slide_start = new Date().getTime();
            slide_timer = setTimeout(function(){
                slide_delay = slide_start = 0; // reset slideshow
                SB.next();
            }, slide_delay);

            // change play nav to pause
            toggleNav('play', false);
            toggleNav('pause', true);
        }
    };

    /**
     * Pauses the current slideshow.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.pause = function(){
        if(typeof slide_timer == 'number'){
            var time = new Date().getTime();
            slide_delay = Math.max(0, slide_delay - (time - slide_start));

            // any delay left on current slide? if so, stop the timer
            if(slide_delay){
                clearTimeout(slide_timer);
                slide_timer = 'paused';
            }

            // change pause nav to play
            toggleNav('pause', false);
            toggleNav('play', true);
        }
    };

    /**
     * Deactivates Shadowbox.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.close = function(){
        if(!activated) return; // already closed

        // stop listening for keys
        listenKeys(false);
        // hide
        toggleVisible(false);
        // remove the content
        if(content){
            content.remove();
            content = null;
        }

        // clear slideshow variables
        if(typeof slide_timer == 'number') clearTimeout(slide_timer);
        slide_timer = null;
        slide_delay = 0;

        // fire onClose hook
        if(options.onClose && typeof options.onClose == 'function') options.onClose(gallery[current]);
        activated = false;
    };

    /**
     * Clears Shadowbox' cache and removes listeners and expandos from all
     * cached link elements. May be used to completely reset Shadowbox in case
     * links on a page change.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.clearCache = function(){
        SL.each(cache, function(c){
            if(c.el){
                SL.removeEvent(c.el, 'click', handleClick);
                try{
                    delete c.el.shadowboxCacheKey; // remove expando
                }catch(e){
                    // use removeAttribute for IE
                    if(c.el.removeAttribute) c.el.removeAttribute('shadowboxCacheKey');
                }
            }
        });
        cache = [];
    };

    /**
     * Gets an object that lists which plugins are supported by the client. The
     * keys of this object will be:
     *
     * - fla: Adobe Flash Player
     * - qt: QuickTime Player
     * - wmp: Windows Media Player
     * - f4m: Flip4Mac QuickTime Player
     *
     * @return  Object          The plugins object
     * @public
     * @static
     */
    Shadowbox.getPlugins = function(){
        return plugins;
    };

    /**
     * Gets the current options object in use.
     *
     * @return  Object          The options object
     * @public
     * @static
     */
    Shadowbox.getOptions = function(){
        return options;
    };

    /**
     * Gets the current gallery object.
     *
     * @return  Object          The current gallery item
     * @public
     * @static
     */
    Shadowbox.getCurrent = function(){
        return gallery[current];
    };

    /**
     * Gets the current version number of Shadowbox.
     *
     * @return  String          The current version
     * @public
     * @static
     */
    Shadowbox.getVersion = function(){
        return version;
    };

    /**
     * Returns an object containing information about the current client
     * configuration.
     *
     * @return  Object          The object containing client data
     * @public
     * @static
     */
    Shadowbox.getClient = function(){
        return client;
    };

    /**
     * Returns the current content object in use.
     *
     * @return  Object          The current content object
     * @public
     * @static
     */
    Shadowbox.getContent = function(){
        return content;
    };

    /**
     * Gets the current dimensions of Shadowbox as calculated by
     * setDimensions().
     *
     * @return  Object          The current dimensions of Shadowbox
     * @public
     * @static
     */
    Shadowbox.getDimensions = function(){
        return dims;
    };

    /**
     * Handles all Shadowbox exceptions (errors). Calls the exception
     * handler callback if one is present (see handleException option) or
     * throws a new exception.
     *
     * @param   String      e       The error message
     * @return  void
     * @public
     * @static
     */
    Shadowbox.raise = function(e){
        if(typeof options.handleException == 'function'){
            options.handleException(e);
        }else{
            throw e;
        }
    };

})();

