/**

 * 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 108 2008-07-11 04:19:01Z mjijackson $

 *

 *

 * To change starting of gallery with sideshow paused, change var slide_timer; 

 * to var slide_timer; Also, find Shadowbox.close = function(){  anc

 * change slide_timer = null; to slide_timer = 'paused';.

 */



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

 *

 * @class       Shadowbox

 * @author      Michael J. I. Jackson <mjijackson@gmail.com>

 * @singleton

 */

(function(){



    /**

     * The current version of Shadowbox.

     *

     * @var         String

     * @private

     */

    var version = '2.0';



    /**

     * Contains the default options for Shadowbox.

     *

     * @var         Object

     * @private

     */

    var options = {



        /**

         * Enable all animations besides fades.

         *

         * @var     Boolean

         */

        animate:            true,



        /**

         * Enable fade animations.

         *

         * @var     Boolean

         */

        animateFade:        true,



        /**

         * Specifies the sequence of the height and width animations. May be

         * 'wh' (width then height), 'hw' (height then width), or 'sync' (both

         * at the same time). Of course this will only work if animate is true.

         *

         * @var     String

         */

        animSequence:       'wh',



        /**

         * The path to flvplayer.swf.

         *

         * @var     String

         */

        flvPlayer:          'flvplayer.swf',



        /**

         * Listen to the overlay for clicks. If the user clicks the overlay,

         * it will trigger Shadowbox.close().

         *

         * @var     Boolean

         */

        modal:              false,



        /**

         * The color to use for the modal overlay (in hex).

         *

         * @var     String

         */

        overlayColor:       '#663600',



        /**

         * The opacity to use for the modal overlay.

         *

         * @var     Number

         */

        overlayOpacity:     0.86,



        /**

         * The default background color to use for Flash movies (in hex).

         *

         * @var     String

         */

        flashBgColor:       '#ffffff',



        /**

         * Automatically play movies.

         *

         * @var     Boolean

         */

        autoplayMovies:     true,



        /**

         * Enable movie controllers on movie players.

         *

         * @var     Boolean

         */

        showMovieControls:  true,



        /**

         * A delay (in seconds) to use for slideshows. If set to anything other

         * than 0, this value determines an interval at which Shadowbox will

         * automatically proceed to the next piece in the gallery.

         *

         * @var     Number

         */

        slideshowDelay:     0,



        /**

         * The duration of the resizing animations (in seconds).

         *

         * @var     Number

         */

        resizeDuration:     0.55,



        /**

         * The duration of the fading animations (in seconds).

         *

         * @var     Number

         */

        fadeDuration:       0.35,



        /**

         * Show the navigation controls.

         *

         * @var     Boolean

         */

        displayNav:         true,



        /**

         * Enable continuous galleries. When this is true, users will be able

         * to skip to the first gallery image from the last using next and vice

         * versa.

         *

         * @var     Boolean

         */

        continuous:         true,



        /**

         * Display the gallery counter.

         *

         * @var     Boolean

         */

        displayCounter:     true,



        /**

         * This option may be either 'default' or 'skip'. The default counter is

         * a simple '1 of 5' message. The skip counter displays a link for each

         * piece in the gallery that enables a user to skip directly to any

         * piece.

         *

         * @var     String

         */

        counterType:        'default',



        /**

         * Limits the number of counter links that will be displayed in a "skip"

         * style counter. If the actual number of gallery elements is greater

         * than this value, the counter will be restrained to the elements

         * immediately preceeding and following the current element.

         *

         * @var     Number

         */

        counterLimit:       10,



        /**

         * The amount of padding to maintain around the viewport edge (in

         * pixels). This only applies when the image is very large and takes up

         * the entire viewport. Default is 20.

         *

         * @var     Number

         */

        viewportPadding:    0,



        /**

         * 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.

         *

         * @var     String

         */

        handleOversize:     'resize',



        /**

         * An exception handling function that will be called whenever

         * Shadowbox should throw an exception. Will be passed the error

         * message as its first argument.

         *

         * @var     Function

         */

        handleException:    null,



        /**

         * 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.

         *

         * @var     String

         */

        handleUnsupported:  'link',



        /**

         * The initial height of Shadowbox (in pixels).

         *

         * @var     Number

         */

        initialHeight:      320,



        /**

         * The initial width of Shadowbox (in pixels).

         *

         * @var     Number

         */

        initialWidth:       320,



        /**

         * Enable keyboard control.

         *

         * @var     Boolean

         */

        enableKeys:         true,



        /**

         * A hook function to be fired when Shadowbox opens. The single argument

         * will be the current gallery element.

         *

         * @var     Function

         */

        onOpen:             null,



        /**

         * A hook function to be fired when Shadowbox finishes loading its

         * content. The single argument will be the current gallery element on

         * display.

         *

         * @var     Function

         */

        onFinish:           null,



        /**

         * A hook function to be fired when Shadowbox changes from one gallery

         * element to the next. The single argument will be the current gallery

         * element that is about to be displayed.

         *

         * @var     Function

         */

        onChange:           null,



        /**

         * A hook function that will be fired when Shadowbox closes. The single

         * argument will be the gallery element most recently displayed.

         *

         * @var     Function

         */

        onClose:            null,



        /**

         * Skips calling Shadowbox.setup() in init(). This means that it must

         * be called later manually.

         *

         * @var     Boolean

         */

        skipSetup:          false,



        /**

         * An object containing names of plugins and links to their respective

         * download pages.

         *

         * @var     Object

         */

        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.

         *

         * @var     Object      ext

         */

        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', 'pl', 'php',

                        'php3', 'php4', 'php5', 'phtml', 'rb', 'rhtml', 'shtml',

                        'txt', 'vbs']

        }



    };



    // 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 = {

        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 = 'shadowbox_content';



    /**

     * Holds the current dimensions of Shadowbox as calculated by

     * setDimensions(). Contains the following properties:

     *

     * - height: The total height of #shadowbox

     * - width: The total width of #shadowbox

     * - inner_h: The height of #shadowbox_body

     * - inner_w: The width of #shadowbox_body

     * - top: The top to use for #shadowbox

     * - 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;



    /**

     * The timeout id for the slideshow transition function.

     *

     * @var         Number

     * @private

     */

    var slide_timer = 'paused';



    /**

     * 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;



    /**

     * These parameters for simple browser detection. Adapted from Ext.js.

     *

     * @var         Object

     * @private

     */

    var ua = navigator.userAgent.toLowerCase();

    var client = {

        isStrict:   document.compatMode == 'CSS1Compat',

        isOpera:    ua.indexOf('opera') > -1,

        isIE:       ua.indexOf('msie') > -1,

        isIE7:      ua.indexOf('msie 7') > -1,

        isSafari:   /webkit|khtml/.test(ua),

        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

    };

    client.isBorderBox = client.isIE && !client.isStrict;

    client.isSafari3 = client.isSafari && !!(document.evaluate);

    client.isGecko = ua.indexOf('gecko') != -1 && !client.isSafari;



    /**

     * You're not sill using IE6 are you?

     *

     * @var         Boolean

     * @private

     */

    var ltIE7 = client.isIE && !client.isIE7;



    /**

     * 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 detectPlugin = function(plugin_name){

            var detected = false;

            for (var i = 0, len = navigator.plugins.length; i < len; ++i){

                if(navigator.plugins[i].name.indexOf(plugin_name) > -1){

                    detected = true;

                    break;

                }

            }

            return detected;

        };

        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(plugin_name){

            var detected = false;

            try{

                var axo = new ActiveXObject(plugin_name);

                if(axo) detected = true;

            }catch(e){}

            return detected;

        };

        plugins = {

            fla:    detectPlugin('ShockwaveFlash.ShockwaveFlash'),

            qt:     detectPlugin('QuickTime.QuickTime'),

            wmp:    detectPlugin('wmplayer.ocx'),

            f4m:    false

        };

    }



    /**

     * 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;

    };



    /**

     * 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');

    };



    /**

     * Gets the height of the viewport in pixels. Note: This function includes

     * scrollbars in Safari 3.

     *

     * @return  Number          The height of the viewport

     * @public

     * @static

     */

    SL.getViewportHeight = function(){

        var h = window.innerHeight; // Safari

        var mode = document.compatMode;

        if((mode || client.isIE) && !client.isOpera){

            h = client.isStrict ? document.documentElement.clientHeight : document.body.clientHeight;

        }

        return h;

    };



    /**

     * Gets the width of the viewport in pixels. Note: This function includes

     * scrollbars in Safari 3.

     *

     * @return  Number          The width of the viewport

     * @public

     * @static

     */

    SL.getViewportWidth = function(){

        var w = window.innerWidth; // Safari

        var mode = document.compatMode;

        if(mode || client.isIE){

            w = client.isStrict ? document.documentElement.clientWidth : document.body.clientWidth;

        }

        return w;

    };



    /**

     * 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

     */

    SL.createHTML = function(obj){

        var html = '<' + obj.tag;

        for(var attr in obj){

            if(attr == 'tag' || attr == 'html' || attr == 'children') continue;

            if(attr == 'cls'){

                html += ' class="' + obj['cls'] + '"';

            }else{

                html += ' ' + attr + '="' + obj[attr] + '"';

            }

        }

        if(RE.empty.test(obj.tag)){

            html += '/>';

        }else{

            html += '>';

            var cn = obj.children;

            if(cn){

                for(var i = 0, len = cn.length; i < len; ++i){

                    html += this.createHTML(cn[i]);

                }

            }

            if(obj.html) html += obj.html;

            html += '</' + obj.tag + '>';

        }

        return html;

    };



    /**

     * 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

     * @private

     * @static

     */

    var ease = function(x){

        return 1 + Math.pow(x - 1, 3);

    };



    /**

     * 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;



        if(from == to){

            if(typeof cb == 'function') cb();

            return; // nothing to animate

        }



        var delta = to - from;

        var op = p == 'opacity';

        var unit = op ? '' : 'px'; // default unit is px

        var fn = function(ease){

            SL.setStyle(el, p, from + ease * delta + unit);

        };



        // cancel the animation here if set in the options

        if(!options.animate && !op || op && !options.animateFade){

            fn(1);

            if(typeof cb == 'function') cb();

            return;

        }



        d *= 1000; // convert to milliseconds

        var begin = new Date().getTime();

        var end = begin + d;



        var timer = setInterval(function(){

            var time = new Date().getTime();

            if(time >= end){ // end of animation

                clearInterval(timer);

                fn(1);

                if(typeof cb == 'function') cb();

            }else{

                fn(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 = '';

            s['-moz-opacity'] = '';

            s['-khtml-opacity'] = '';

        }

    };



    /**

     * Gets the computed height of the given element, including padding and

     * borders.

     *

     * @param   HTMLElement     el  The element

     * @return  Number              The computed height of the element

     * @private

     */

    var getComputedHeight = function(el){

        var h = Math.max(el.offsetHeight, el.clientHeight);

        if(!h){

            h = parseInt(SL.getStyle(el, 'height'), 10) || 0;

            if(!client.isBorderBox){

                h += parseInt(SL.getStyle(el, 'padding-top'), 10)

                    + parseInt(SL.getStyle(el, 'padding-bottom'), 10)

                    + parseInt(SL.getStyle(el, 'border-top-width'), 10)

                    + parseInt(SL.getStyle(el, 'border-bottom-width'), 10);

            }

        }

        return h;

    };



    /**

     * 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);

        var 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('shadowbox_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];

        var title_i = SL.get('shadowbox_title_inner');



        // build the title

        title_i.innerHTML = obj.title || '';



        // build the nav

        var nav = SL.get('shadowbox_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

        var counter = SL.get('shadowbox_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){ // support large galleries

                        var h = Math.round(limit / 2);

                        i = current - h;

                        if(i < 0) i += len;

                        end = current + (limit - h);

                        if(end > len) end -= len;

                    }

                    while(i != end){

                        if(i == len) i = 0;

                        co += '<a onclick="Shadowbox.change(' + i + ');"';

                        if(i == current) co += ' class="shadowbox_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 obj = gallery[current];

        var title = SL.get('shadowbox_title');

        var info = SL.get('shadowbox_info');

        var title_i = SL.get('shadowbox_title_inner');

        var info_i = SL.get('shadowbox_info_inner');



        // build bars after they are hidden

        var fn = function(){

            buildBars(cb);

        };



        var title_h = getComputedHeight(title);

        var info_h = getComputedHeight(info) * -1;

        if(anim){

            // animate the transition

            animate(title_i, 'margin-top', title_h, 0.35);

            animate(info_i, 'margin-top', info_h, 0.35, fn);

        }else{

            SL.setStyle(title_i, 'margin-top', title_h + 'px');

            SL.setStyle(info_i, 'margin-top', info_h + '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 title_i = SL.get('shadowbox_title_inner');

        var info_i = SL.get('shadowbox_info_inner');

        var t = title_i.innerHTML != ''; // is there a title to display?



        if(t) animate(title_i, 'margin-top', 0, 0.35);

        animate(info_i, 'margin-top', 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 #shadowbox display

            if(!changing){

                 SL.get('shadowbox').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('shadowbox_body_inner').innerHTML = SL.createHTML(content.markup(dims));



                        toggleLoading(false, function(){

                            if(!content) return;



                            if(typeof content.onLoad == 'function'){

                                content.onLoad(); // call onLoad callback if present

                            }

                            if(options.onFinish && typeof options.onFinish == 'function'){

                                options.onFinish(gallery[current]); // fire onFinish handler

                            }

                            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 the shadowbox_body. If the height/width

     * combination is too large for Shadowbox and handleOversize option is set

     * to 'resize', 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('shadowbox_body');

        var h = height = parseInt(height);

        var w = width = parseInt(width);

        var view_h = SL.getViewportHeight();

        var view_w = SL.getViewportWidth();



        // calculate the max width

        var border_w = parseInt(SL.getStyle(sb, 'border-left-width'), 10)

            + parseInt(SL.getStyle(sb, 'border-right-width'), 10);

        var extra_w = border_w + 2 * options.viewportPadding;

        if(w + extra_w >= view_w){

            w = view_w - extra_w;

        }



        // calculate the max height

        var border_h = parseInt(SL.getStyle(sb, 'border-top-width'), 10)

            + parseInt(SL.getStyle(sb, 'border-bottom-width'), 10);

        var bar_h = getComputedHeight(SL.get('shadowbox_title'))

            + getComputedHeight(SL.get('shadowbox_info'));

        var extra_h = border_h + 2 * options.viewportPadding + bar_h;

        if(h + extra_h >= view_h){

            h = view_h - extra_h;

        }



        // handle oversized content

        var drag = false;

        var resize_h = height;

        var resize_w = width;

        var handle = options.handleOversize;

        if(resizable && (handle == 'resize' || handle == 'drag')){

            var change_h = (height - h) / height;

            var 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 + border_h + bar_h,

            width:      w + border_w,

            inner_h:    h,

            inner_w:    w,

            top:        (view_h - (h + extra_h)) / 2 + options.viewportPadding,

            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(cb){

            switch(options.animSequence){

                case 'hw':

                    adjustHeight(dims.inner_h, dims.top, true, function(){

                        adjustWidth(dims.width, true, cb);

                    });

                    break;

                case 'wh':

                    adjustWidth(dims.width, true, function(){

                        adjustHeight(dims.inner_h, dims.top, true, cb);

                    });

                    break;

                case 'sync':

                default:

                    adjustWidth(dims.width, true);

                    adjustHeight(dims.inner_h, dims.top, true, cb);

            }

        }else{ // window resize

            adjustWidth(dims.width, 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){

                        SL.setStyle(c, 'top', dims.inner_h - content.height + 'px');

                    }

                    var left = parseInt(SL.getStyle(c, 'left'));

                    if(left + content.width < dims.inner_w){

                        SL.setStyle(c, 'left', dims.inner_w - content.width + 'px');

                    }

                }

            }

        }

    };



    /**

     * Adjusts the height of #shadowbox_body and centers #shadowbox vertically

     * in the viewport.

     *

     * @param   Number      height      The height to use for #shadowbox_body

     * @param   Number      top         The top to use for #shadowbox

     * @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){

        height = parseInt(height);



        // adjust the height

        var sb = SL.get('shadowbox_body');

        if(anim){

            animate(sb, 'height', height, options.resizeDuration);

        }else{

            SL.setStyle(sb, 'height', height + 'px');

        }



        // adjust the top

        var s = SL.get('shadowbox');

        if(anim){

            animate(s, 'top', top, options.resizeDuration, cb);

        }else{

            SL.setStyle(s, 'top', top + 'px');

            if(typeof cb == 'function') cb();

        }

    };



    /**

     * Adjusts the width of #shadowbox.

     *

     * @param   Number      width       The width to use for #shadowbox

     * @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, anim, cb){

        width = parseInt(width);



        // adjust the width

        var s = SL.get('shadowbox');

        if(anim){

            animate(s, 'width', width, options.resizeDuration, cb);

        }else{

            SL.setStyle(s, 'width', width + 'px');

            if(typeof cb == 'function') 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 loading = SL.get('shadowbox_loading');

        if(on){

            loading.style.display = '';

            if(typeof cb == 'function') cb();

        }else{

            var p = gallery[current].player;

            var anim = (p == 'img' || p == 'html'); // fade on images & html

            var fn = function(){

                loading.style.display = 'none';

                clearOpacity(loading);

                if(typeof cb == 'function') cb();

            };

            if(anim){

                animate(loading, '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('shadowbox_container').style.top = document.documentElement.scrollTop + 'px';

    };



    /**

     * Sets the height of the overlay element to the full viewport height. This

     * is only necessary in IE6 where the container uses absolute positioning

     * instead of fixed, thus restricting the size of the overlay element.

     *

     * @return  void

     * @private

     */

    var fixHeight = function(){

        SL.get('shadowbox_overlay').style.height = SL.getViewportHeight() + '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 #shadowbox_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 els, v = (cb) ? 'hidden' : 'visible';

        var hide = ['select', 'object', 'embed']; // tags to hide

        for(var i = 0; i < hide.length; ++i){

            els = document.getElementsByTagName(hide[i]);

            for(var j = 0, len = els.length; j < len; ++j){

                els[j].style.visibility = v;

            }

        }



        // resize & show container

        var so = SL.get('shadowbox_overlay');

        var sc = SL.get('shadowbox_container');

        var sb = SL.get('shadowbox');

        if(cb){

            // set overlay color/opacity

            SL.setStyle(so, {

                backgroundColor: options.overlayColor,

                opacity: 0

            });

            if(!options.modal) SL.addEvent(so, 'click', SB.close);

            if(ltIE7){

                // fix container top & overlay height before showing

                fixTop();

                fixHeight();

                SL.addEvent(window, 'scroll', fixTop);

            }



            // fade in animation

            sb.style.display = 'none'; // will be cleared in loadContent()

            sc.style.visibility = 'visible';

            animate(so, 'opacity', parseFloat(options.overlayOpacity), options.fadeDuration, cb);

        }else{

            SL.removeEvent(so, 'click', SB.close);

            if(ltIE7) SL.removeEvent(window, 'scroll', fixTop);



            // fade out effect

            sb.style.display = 'none';

            animate(so, 'opacity', 0, options.fadeDuration, function(){

                sc.style.visibility = 'hidden';

                sb.style.display = '';

                clearOpacity(so);

            });

        }

    };



    /**

     * 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 custom options

        apply(options, opts || {});



        // add markup

        var markup = SB.SKIN.markup.replace(/\{(\w+)\}/g, function(m, p){

            return SB.LANG[p];

        });

        var bd = document.body || document.documentElement;

        SL.append(bd, markup);



        // several fixes for IE6

        if(ltIE7){

            // give the container absolute positioning

            SL.setStyle(SL.get('shadowbox_container'), 'position', 'absolute');

            // give shadowbox_body "layout"...whatever that is

            SL.get('shadowbox_body').style.zoom = 1;

            // use AlphaImageLoader for transparent PNG support

            var png = SB.SKIN.png_fix;

            if(png && png.constructor == Array){

                for(var i = 0; i < png.length; ++i){

                    var el = SL.get(png[i]);

                    if(el){

                        var match = SL.getStyle(el, 'background-image').match(/url\("(.*\.png)"\)/);

                        if(match){

                            SL.setStyle(el, {

                                backgroundImage: 'none',

                                filter: 'progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true,src=' + match[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;

            }

            id = setTimeout(function(){

                if(ltIE7) fixHeight();

                resizeContent();

            }, 50);

        });



        if(!options.skipSetup) SB.setup();

        initialized = true;

    };



    /**

     * 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 + '/';



        // Safari 2.0 fails using DOM, use document.write instead

        document.write('<link rel="stylesheet" type="text/css" href="' + skin + 'skin.css">');

        document.write('<scr' + 'ipt type="text/javascript" src="' + skin + 'skin.js"><\/script>');

    };



    /**

     * 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 += '/';



        // Safari 2.0 fails using DOM, use document.write instead

        document.write('<scr' + 'ipt type="text/javascript" src="' + dir + 'shadowbox-' + lang + '.js"><\/script>');

    };



    /**

     * 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 += '/';



        for(var i = 0, len = players.length; i < len; ++i){

            // Safari 2.0 fails using DOM, use document.write instead

            document.write('<scr' + 'ipt type="text/javascript" src="' + dir + 'shadowbox-' + players[i] + '.js"><\/script>');

        }

    };



    /**

     * 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 = [];

            var a = document.getElementsByTagName('a'), rel;

            for(var i = 0, len = a.length; i < len; ++i){

                rel = a[i].getAttribute('rel');

                if(rel && RE.rel.test(rel)) links[links.length] = a[i];

            }

        }else if(!links.length){

            links = [links]; // one link

        }



        var link;

        for(var i = 0, len = links.length; i < len; ++i){

            link = links[i];

            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);

        }

    };



    /**

     * 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

        var opt, l_opts = ['player', 'title', 'height', 'width', 'gallery'];

        for(var i = 0, len = l_opts.length; i < len; ++i){

            opt = l_opts[i];

            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

            var params = rel.split(';');

            for(var i = 0, len = params.length; i < len; ++i){

                match = params[i].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

                var ci;

                for(var i = 0, len = cache.length; i < len; ++i){

                    ci = cache[i];

                    if(ci.gallery){

                        if(ci.content == obj.content

                            && ci.gallery == obj.gallery

                            && ci.title == obj.title){ // compare content, gallery, & title

                                current = gallery.length; // key element found

                        }

                        if(ci.gallery == obj.gallery){

                            gallery.push(apply({}, ci));

                        }

                    }

                }

                // 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;

        for(var i = 0, len = gallery.length; i < len; ++i){

            r = false; // remove the element?

            if(gallery[i].player == 'unsupported'){ // don't support this at all

                r = true;

            }else if(match = RE.unsupported.exec(gallery[i].player)){ // handle unsupported elements

                if(options.handleUnsupported == 'link'){

                    gallery[i].player = 'html';

                    // generate a link to the appropriate plugin download page(s)

                    var s, a, oe = options.errors;

                    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];

                    }

                    var msg = SB.LANG.errors[s].replace(/\{(\d+)\}/g, function(m, i){

                        return a[i];

                    });

                    gallery[i].content = '<div class="shadowbox_message">' + msg + '</div>';

                }else{

                    r = true;

                }

            }else if(gallery[i].player == 'inline'){ // handle inline elements

                // retrieve the innerHTML of the inline element

                var match = RE.inline.exec(gallery[i].content);

                if(match){

                    var el;

                    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?

        if(gallery.length){

            // fire onOpen hook

            if(options.onOpen && typeof options.onOpen == 'function'){

                options.onOpen(obj);

            }



            if(!activated){

                // set initial dimensions & load

                setDimensions(options.initialHeight, options.initialWidth);

                adjustHeight(dims.inner_h, dims.top, false);

                adjustWidth(dims.width, 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



        if(options.onChange && typeof options.onChange == 'function'){

            options.onChange(gallery[current]); // fire onChange handler

        }



        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 = 'paused';

        slide_delay = 0;



        // fire onClose handler

        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(){

        for(var i = 0, len = cache.length; i < len; ++i){

            if(cache[i].el){

                SL.removeEvent(cache[i].el, 'click', handleClick);

                delete cache[i].el.shadowboxCacheKey; // remove expando

            }

        }

        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;

        }

    };



})();