Index: /trunk/src/wp-admin/admin-ajax.php
===================================================================
--- /trunk/src/wp-admin/admin-ajax.php	(revision 27418)
+++ /trunk/src/wp-admin/admin-ajax.php	(revision 27419)
@@ -59,5 +59,5 @@
 	'query-attachments', 'save-attachment', 'save-attachment-compat', 'send-link-to-editor',
 	'send-attachment-to-editor', 'save-attachment-order', 'heartbeat', 'get-revision-diffs',
-	'save-user-color-scheme',
+	'save-user-color-scheme', 'update-widget',
 );
 
Index: /trunk/src/wp-admin/css/customize-widgets.css
===================================================================
--- /trunk/src/wp-admin/css/customize-widgets.css	(revision 27419)
+++ /trunk/src/wp-admin/css/customize-widgets.css	(revision 27419)
@@ -0,0 +1,623 @@
+.wp-full-overlay-sidebar {
+	overflow: visible;
+}
+
+/**
+ * Hide all sidebar sections by default, only show them (via JS) once the
+ * preview loads and we know whether the sidebars are used in the template.
+ */
+
+.control-section[id^="accordion-section-sidebar-widgets-"],
+.customize-control-sidebar_widgets label,
+.customize-control-sidebar_widgets .hide-if-js {
+	/* The link in .customize-control-sidebar_widgets .hide-if-js will fail if it ever gets used. */
+	display:none;
+}
+
+.customize-control-widget_form .widget-top {
+	-webkit-transition: opacity 0.5s;
+	transition: opacity 0.5s;
+}
+
+.customize-control-widget_form:not(.widget-rendered) .widget-top {
+	opacity: 0.5;
+}
+
+
+.customize-control-widget_form .widget-control-save {
+	display: none;
+}
+
+.customize-control-widget_form .spinner {
+	display: inline;
+	opacity: 0.0;
+	-webkit-transition: opacity 0.1s;
+	transition: opacity 0.1s;
+}
+.customize-control-widget_form.previewer-loading .spinner {
+	opacity: 1.0;
+}
+
+.customize-control-widget_form .widget {
+	margin-bottom: 0;
+}
+
+.customize-control-widget_form:not(.wide-widget-control) {
+	/**
+	 * Prevent plugins (e.g. Widget Visibility in Jetpack) from forcing widget forms
+	 * to be wide and so overflow the customizer panel
+	 */
+	left: auto !important;
+	max-width: 100%;
+}
+.customize-control-widget_form.wide-widget-control .widget-inside {
+	position: fixed;
+	left: 299px;
+	top: 25%;
+	padding: 20px;
+	border: 1px solid rgb(229, 229, 229);
+	z-index: -1;
+}
+.customize-control-widget_form.wide-widget-control.collapsing .widget-inside {
+	z-index: -2;
+}
+
+.customize-control-widget_form.wide-widget-control .widget-top {
+	-webkit-transition: background-color 0.4s;
+	transition: background-color 0.4s;
+}
+.customize-control-widget_form.wide-widget-control.expanding .widget-top,
+.customize-control-widget_form.wide-widget-control.expanded:not(.collapsing) .widget-top {
+	background-color: rgb(227, 227, 227);
+}
+
+.widget-inside {
+	padding: 1px 10px 10px 10px;
+	border-top: none;
+	line-height: 16px;
+}
+
+.widget-top {
+	cursor: move;
+}
+
+.customize-control-widget_form.expanded a.widget-action:after {
+	content: "\f142";
+}
+
+.customize-control-widget_form.wide-widget-control a.widget-action:after {
+	content: "\f139";
+}
+
+.customize-control-widget_form.wide-widget-control.expanded a.widget-action:after {
+	content: "\f141";
+}
+
+.widget-title-action {
+	cursor: pointer;
+}
+
+.customize-control-widget_form .widget .customize-control-title {
+	cursor: move;
+}
+
+/* @todo What does this do? */
+.control-section.accordion-section.widget-customizer-highlighted > .accordion-section-title,
+.customize-control-widget_form.widget-customizer-highlighted {
+	outline: none;
+	-webkit-box-shadow: 0 0 3px #ce0000;
+	box-shadow: 0 0 3px #ce0000;
+}
+
+#widget-customizer-control-templates {
+	display: none;
+}
+
+
+/* MP6-compat */
+#customize-theme-controls .accordion-section-content .widget {
+	color: black;
+}
+
+
+/**
+* Widget reordering styles
+**/
+
+.reorder-toggle {
+	float: right;
+	padding: 5px 10px;
+	margin-right: 10px;
+	text-decoration: none;
+	cursor: pointer;
+	outline: none;
+	-webkit-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+}
+.reorder-toggle:focus {
+	outline: 1px dotted;
+}
+
+.reorder-done,
+.reordering .reorder {
+	display: none;
+}
+
+.reordering .reorder-done {
+	display: block;
+	color: #aa0000;
+}
+
+#customize-theme-controls .reordering .add-new-widget {
+	opacity: 0.2;
+	pointer-events: none;
+	cursor: not-allowed;
+}
+
+#customize-theme-controls .widget-reorder-nav {
+	display: none;
+	float: right;
+	background-color: #fafafa;
+}
+
+.widget-reorder-nav span {
+	position: relative;
+	overflow: hidden;
+	float: left;
+	display: block;
+	width: 33px; /* was 42px for mobile */
+	height: 43px;
+	color: #888;
+	text-indent: -9999px;
+	cursor: pointer;
+	outline: none;
+}
+
+.widget-reorder-nav span:before {
+	display: inline-block;
+	position: absolute;
+	top: 0;
+	right: 0;
+	width: 100%;
+	height: 100%;
+	font: normal normal 20px/43px 'Genericons';
+	text-align: center;
+	text-indent: 0;
+}
+
+.widget-reorder-nav span:hover,
+.widget-reorder-nav span:focus {
+	color: #444;
+	background: #eee;
+}
+
+.move-widget:before {
+	content: '\f442';
+}
+
+.move-widget-down:before {
+	content: '\f431';
+}
+
+.move-widget-up:before {
+	content: '\f432';
+}
+
+#customize-theme-controls .first-widget .move-widget-up,
+#customize-theme-controls .last-widget .move-widget-down {
+	color: #d5d5d5;
+	cursor: default;
+}
+
+#customize-theme-controls  .move-widget-area {
+	display: none;
+	background: #fff;
+	border: 1px solid #dedede;
+	border-top: none;
+	cursor: auto;
+}
+
+#customize-theme-controls .reordering .move-widget-area.active {
+	display: block;
+}
+
+#customize-theme-controls .move-widget-area .description {
+	margin: 0;
+	padding: 15px 20px;
+	font-weight: 400;
+}
+
+#customize-theme-controls .widget-area-select {
+	margin: 0;
+	padding: 0;
+	list-style: none;
+}
+
+#customize-theme-controls .widget-area-select li {
+	position: relative;
+	margin: 0;
+	padding: 13px 15px 15px 42px;
+	color: #555;
+	border-top: 1px solid #eee;
+	cursor: pointer;
+	-webkit-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+}
+
+#customize-theme-controls .widget-area-select li:before {
+	display: none;
+	content: '\f418';
+	position: absolute;
+	top: 10px;
+	left: 10px;
+	font-family: 'Genericons';
+	font-size: 24px;
+	line-height: 1;
+}
+
+#customize-theme-controls .widget-area-select li:last-child {
+	border-bottom: 1px solid #eee;
+}
+
+#customize-theme-controls .widget-area-select .selected {
+	color: #fff;
+	text-shadow: 0 -1px 0 rgba(0,0,0,.4);
+	border-top: 1px solid #207fa1;
+	background: #2ea2cc;
+}
+
+#customize-theme-controls .widget-area-select .selected:before {
+	display: block;
+}
+
+#customize-theme-controls .widget-area-select .selected:last-child {
+	border-bottom: 1px solid #207fa1;
+}
+
+#customize-theme-controls .move-widget-actions {
+	text-align: right;
+	padding: 12px;
+}
+
+#customize-theme-controls .widget-area-select + li {
+	border-top: 1px solid #207fa1;
+}
+
+#customize-theme-controls .reordering .widget-title-action {
+	display: none;
+}
+
+#customize-theme-controls .reordering .widget-reorder-nav {
+	display: block;
+}
+
+
+/**
+ * Styles for new widget addition panel
+ */
+.wp-full-overlay-main {
+	right: auto; /* this overrides a right: 0; which causes the preview to resize, I'd rather have it go off screen at the normal size. */
+	width: 100%;
+}
+
+.add-new-widget {
+	cursor: pointer;
+	float: right;
+	-webkit-transition: all 0.2s;
+	transition: all 0.2s;
+	-webkit-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+	-moz-outline: none;
+	outline: none;
+}
+
+.add-new-widget:before {
+	content: "\f132";
+	display: inline-block;
+	position: relative;
+		left: -2px;
+		top: -1px;
+	font: normal 16px/1 'dashicons';
+	vertical-align: middle;
+	-webkit-transition: all 0.2s;
+	transition: all 0.2s;
+	-webkit-font-smoothing: antialiased;
+}
+
+body.adding-widget .add-new-widget,
+body.adding-widget .add-new-widget:hover {
+	background: #EEE;
+	border-color: #999;
+	color: #333;
+	-webkit-box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5);
+	box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5);
+}
+body.adding-widget .add-new-widget:before {
+	-webkit-transform: rotate(45deg);
+	-ms-transform: rotate(45deg);
+	transform: rotate(45deg);
+}
+
+#available-widgets .widget {
+	position: static;
+}
+
+/* override widgets admin page rules in wp-admin/css/wp-admin.css */
+#widgets-left #available-widgets .widget {
+	float: none !important;
+	width: auto !important;
+}
+
+#available-widgets {
+	position: absolute;
+	overflow: auto;
+	top: 0;
+	bottom: 0;
+	left: -301px;
+	width: 300px;
+	margin: 0;
+	z-index: 1;
+	background: #fff;
+	-webkit-transition: all 0.2s;
+	transition: all 0.2s;
+	border-right: 1px solid #dddddd;
+}
+
+#available-widgets-filter {
+	padding: 8px 17px 7px 13px;
+	border-bottom: 1px solid #e4e4e4;
+	-webkit-box-sizing: border-box;
+	-moz-box-sizing: border-box;
+	box-sizing: border-box;
+}
+
+#available-widgets-filter input {
+	padding: 5px 10px 2px 10px;
+	width: 100%;
+}
+
+#available-widgets .widget-tpl {
+	position: relative;
+	padding: 20px 15px 20px 60px;
+	border-bottom: 1px solid #e4e4e4;
+	cursor: pointer;
+}
+
+#available-widgets .widget-tpl:hover,
+#available-widgets .widget-tpl.selected {
+	background: #fafafa;
+}
+
+#available-widgets .widget-top,
+#available-widgets .widget-top:hover {
+	border: none;
+	background: transparent;
+	-webkit-box-shadow: none;
+	box-shadow: none;
+}
+
+#available-widgets .widget-title h4 {
+	padding: 0 0 5px;
+	font-size: 14px;
+}
+
+#available-widgets .widget .widget-description {
+	padding: 0;
+	color: #777;
+}
+
+#customize-preview {
+	-webkit-transition: all 0.2s;
+	transition: all 0.2s;
+}
+
+body.adding-widget #available-widgets {
+	left: 0;
+}
+
+body.adding-widget .wp-full-overlay-main {
+	left: 300px;
+}
+
+body.adding-widget #customize-preview {
+	opacity: 0.4;
+}
+
+
+/** Widget Icon styling **
+
+* No plurals in naming.
+* Ordered from lowest to highest specificity.
+
+**/
+#available-widgets .widget-title {
+	position: relative;
+}
+
+#available-widgets .widget-title:before {
+	content:"\f132";
+	position: absolute;
+	top: -3px;
+	right: 100%;
+	margin-right: 20px;
+	width: 20px;
+	height: 20px;
+	color: #333;
+	font: normal 20px/1 'dashicons', 'widgeticons';
+	text-align: center;
+	-webkit-border-radius: 2px;
+	border-radius: 2px;
+	-webkit-box-sizing: border-box;
+	-moz-box-sizing: border-box;
+	box-sizing: border-box;
+	-webkit-font-smoothing: antialiased;
+}
+
+/* smiley */
+#available-widgets [class*="easy"] .widget-title:before { content: "\f328"; top: -4px; }
+
+/* star-filled */
+#available-widgets [class*="super"] .widget-title:before,
+#available-widgets [class*="like"] .widget-title:before { content: "\f155"; top: -4px; }
+
+/* wordpress */
+#available-widgets [class*="meta"] .widget-title:before { content: "\f120"; }
+
+/* archive-box */
+#available-widgets [class*="archives"] .widget-title:before { content: "\f483"; top: -4px; }
+
+/* category */
+#available-widgets [class*="categor"] .widget-title:before { content: "\f318"; top: -4px; }
+
+/* comments */
+#available-widgets [class*="comment"] .widget-title:before,
+#available-widgets [class*="testimonial"] .widget-title:before,
+#available-widgets [class*="chat"] .widget-title:before { content: "\f101"; }
+
+/* post */
+#available-widgets [class*="post"] .widget-title:before { content: "\f109"; }
+
+/* admin-page */
+#available-widgets [class*="page"] .widget-title:before { content: "\f105"; }
+
+/* text */
+#available-widgets [class*="text"] .widget-title:before { content: "\f480"; }
+
+/* links */
+#available-widgets [class*="link"] .widget-title:before { content: "\f103"; }
+
+/* search */
+#available-widgets [class*="search"] .widget-title:before { content: "\f179"; }
+
+/* menu */
+#available-widgets [class*="menu"] .widget-title:before,
+#available-widgets [class*="nav"] .widget-title:before { content: "\f333"; }
+
+/* tag-cloud */
+#available-widgets [class*="tag"] .widget-title:before { content: "\f481"; }
+
+/* rss */
+#available-widgets [class*="rss"] .widget-title:before { content: "\f303"; top: -6px; }
+
+/* calendar */
+#available-widgets [class*="event"] .widget-title:before,
+#available-widgets [class*="calendar"] .widget-title:before { content: "\f145"; top: -4px;}
+
+/* format-image */
+#available-widgets [class*="image"] .widget-title:before,
+#available-widgets [class*="photo"] .widget-title:before,
+#available-widgets [class*="slide"] .widget-title:before,
+#available-widgets [class*="instagram"] .widget-title:before { content: "\f128"; }
+
+/* format-gallery */
+#available-widgets [class*="album"] .widget-title:before,
+#available-widgets [class*="galler"] .widget-title:before { content: "\f161"; }
+
+/* format-video */
+#available-widgets [class*="video"] .widget-title:before,
+#available-widgets [class*="tube"] .widget-title:before { content: "\f126"; }
+
+/* format-audio */
+#available-widgets [class*="music"] .widget-title:before,
+#available-widgets [class*="radio"] .widget-title:before,
+#available-widgets [class*="audio"] .widget-title:before { content: "\f127"; }
+
+/* admin-users */
+#available-widgets [class*="login"] .widget-title:before,
+#available-widgets [class*="user"] .widget-title:before,
+#available-widgets [class*="member"] .widget-title:before,
+#available-widgets [class*="avatar"] .widget-title:before,
+#available-widgets [class*="subscriber"] .widget-title:before,
+#available-widgets [class*="profile"] .widget-title:before,
+#available-widgets [class*="grofile"] .widget-title:before { content: "\f110"; }
+
+/* cart */
+#available-widgets [class*="commerce"] .widget-title:before,
+#available-widgets [class*="shop"] .widget-title:before,
+#available-widgets [class*="cart"] .widget-title:before { content: "\f174"; top: -4px; }
+
+/* shield */
+#available-widgets [class*="secur"] .widget-title:before,
+#available-widgets [class*="firewall"] .widget-title:before { content: "\f332"; }
+
+/* chart-bar */
+#available-widgets [class*="analytic"] .widget-title:before,
+#available-widgets [class*="stat"] .widget-title:before,
+#available-widgets [class*="poll"] .widget-title:before { content: "\f185"; }
+
+/* feedback */
+#available-widgets [class*="form"] .widget-title:before { content: "\f175"; }
+
+/* email-alt */
+#available-widgets [class*="subscribe"] .widget-title:before,
+#available-widgets [class*="news"] .widget-title:before,
+#available-widgets [class*="contact"] .widget-title:before,
+#available-widgets [class*="mail"] .widget-title:before { content: "\f466"; }
+
+/* share */
+#available-widgets [class*="share"] .widget-title:before,
+#available-widgets [class*="socia"] .widget-title:before { content: "\f237"; }
+
+/* translation */
+#available-widgets [class*="lang"] .widget-title:before,
+#available-widgets [class*="translat"] .widget-title:before { content: "\f326"; }
+
+/* location-alt */
+#available-widgets [class*="locat"] .widget-title:before,
+#available-widgets [class*="map"] .widget-title:before { content: "\f231"; }
+
+/* download */
+#available-widgets [class*="download"] .widget-title:before { content: "\f316"; }
+
+/* cloud */
+#available-widgets [class*="weather"] .widget-title:before { content: "\f176"; top: -4px;}
+
+/* facebook */
+#available-widgets [class*="facebook"] .widget-title:before { content: "\f304"; }
+
+/* twitter */
+#available-widgets [class*="tweet"] .widget-title:before,
+#available-widgets [class*="twitter"] .widget-title:before { content: "\f301"; }
+
+
+@media screen and (max-height: 700px) and (min-width: 981px) {
+	.customize-control {
+		margin-bottom: 0;
+	}
+	.widget-top {
+		-webkit-box-shadow: none;
+		box-shadow: none;
+		margin-top: -1px;
+	}
+	.widget-top:hover {
+		position: relative;
+		z-index: 1;
+	}
+	.last-widget {
+		margin-bottom: 15px;
+	}
+	.widget-title h4 {
+		padding: 13px 15px;
+	}
+	.widget-top a.widget-action:after {
+		padding-top: 9px;
+	}
+	.widget-reorder-nav span {
+		height: 39px;
+	}
+	.widget-reorder-nav span:before {
+		line-height: 39px;
+	}
+	#customize-theme-controls .widget-area-select li {
+		padding: 9px 15px 11px 42px;
+	}
+	#customize-theme-controls .widget-area-select li:before {
+		top: 6px;
+	}
+}
Index: /trunk/src/wp-admin/includes/ajax-actions.php
===================================================================
--- /trunk/src/wp-admin/includes/ajax-actions.php	(revision 27418)
+++ /trunk/src/wp-admin/includes/ajax-actions.php	(revision 27419)
@@ -1588,4 +1588,11 @@
 }
 
+function wp_ajax_update_widget() {
+	require( ABSPATH . WPINC . '/class-wp-customize-manager.php' );
+	$GLOBALS['wp_customize'] = new WP_Customize_Manager;
+
+	WP_Customize_Widgets::wp_ajax_update_widget();
+}
+
 function wp_ajax_upload_attachment() {
 	check_ajax_referer( 'media-form' );
Index: /trunk/src/wp-admin/js/customize-widgets.js
===================================================================
--- /trunk/src/wp-admin/js/customize-widgets.js	(revision 27419)
+++ /trunk/src/wp-admin/js/customize-widgets.js	(revision 27419)
@@ -0,0 +1,1733 @@
+/*global wp, Backbone, _, jQuery, WidgetCustomizer_exports */
+/*exported WidgetCustomizer */
+var WidgetCustomizer = ( function ($) {
+	'use strict';
+
+	var customize = wp.customize;
+	var self = {
+		update_widget_ajax_action: null,
+		update_widget_nonce_value: null,
+		update_widget_nonce_post_key: null,
+		i18n: {
+			save_btn_label: '',
+			save_btn_tooltip: '',
+			remove_btn_label: '',
+			remove_btn_tooltip: ''
+		},
+		available_widgets: [], // available widgets for instantiating
+		registered_widgets: [], // all widgets registered
+		active_sidebar_control: null,
+		previewer: null,
+		saved_widget_ids: {},
+		registered_sidebars: [],
+		tpl: {
+			move_widget_area: '',
+			widget_reorder_nav: ''
+		}
+	};
+	$.extend( self, WidgetCustomizer_exports );
+
+	// Lots of widgets expect this old ajaxurl global to be available
+	if ( typeof window.ajaxurl === 'undefined' ) {
+		window.ajaxurl = wp.ajax.settings.url;
+	}
+
+	// Unfortunately many widgets try to look for instances under div#widgets-right,
+	// so we have to add that ID to a container div in the customizer for compat
+	$( '#customize-theme-controls' ).closest( 'div:not([id])' ).attr( 'id', 'widgets-right' );
+
+	/**
+	 * Set up model
+	 */
+	var Widget = self.Widget = Backbone.Model.extend( {
+		id: null,
+		temp_id: null,
+		classname: null,
+		control_tpl: null,
+		description: null,
+		is_disabled: null,
+		is_multi: null,
+		multi_number: null,
+		name: null,
+		id_base: null,
+		transport: 'refresh',
+		params: [],
+		width: null,
+		height: null
+	} );
+	var WidgetCollection = self.WidgetCollection = Backbone.Collection.extend( {
+		model: Widget
+	} );
+	self.available_widgets = new WidgetCollection( self.available_widgets );
+
+	var Sidebar = self.Sidebar = Backbone.Model.extend( {
+		after_title: null,
+		after_widget: null,
+		before_title: null,
+		before_widget: null,
+		'class': null,
+		description: null,
+		id: null,
+		name: null,
+		is_rendered: false
+	} );
+	var SidebarCollection = self.SidebarCollection = Backbone.Collection.extend( {
+		model: Sidebar
+	} );
+	self.registered_sidebars = new SidebarCollection( self.registered_sidebars );
+
+	/**
+	 * On DOM ready, initialize some meta functionality independent of specific
+	 * customizer controls.
+	 */
+	self.init = function () {
+		this.showFirstSidebarIfRequested();
+		this.availableWidgetsPanel.setup();
+	};
+	wp.customize.bind( 'ready', function () {
+		self.init();
+	} );
+
+	/**
+	 * Listen for updates to which sidebars are rendered in the preview and toggle
+	 * the customizer sections accordingly.
+	 */
+	self.showFirstSidebarIfRequested = function () {
+		if ( ! /widget-customizer=open/.test( location.search ) ) {
+			return;
+		}
+
+		var show_first_visible_sidebar = function () {
+			self.registered_sidebars.off( 'change:is_rendered', show_first_visible_sidebar );
+			var first_rendered_sidebar = self.registered_sidebars.find( function ( sidebar ) {
+				return sidebar.get( 'is_rendered' );
+			} );
+			if ( ! first_rendered_sidebar ) {
+				return;
+			}
+			var section = $( '#accordion-section-sidebar-widgets-' + first_rendered_sidebar.get( 'id' ) );
+			if ( ! section.hasClass( 'open' ) ) {
+				section.find( '.accordion-section-title' ).trigger( 'click' );
+			}
+			section[0].scrollIntoView();
+		};
+		show_first_visible_sidebar = _.debounce( show_first_visible_sidebar, 100 ); // so only fires when all updated at end
+		self.registered_sidebars.on( 'change:is_rendered', show_first_visible_sidebar );
+	};
+
+	/**
+	 * Sidebar Widgets control
+	 * Note that 'sidebar_widgets' must match the Sidebar_Widgets_WP_Customize_Control::$type
+	 */
+	customize.controlConstructor.sidebar_widgets = customize.Control.extend( {
+
+		/**
+		 * Set up the control
+		 */
+		ready: function() {
+			var control = this;
+			control.control_section = control.container.closest( '.control-section' );
+			control.section_content = control.container.closest( '.accordion-section-content' );
+			control._setupModel();
+			control._setupSortable();
+			control._setupAddition();
+			control._applyCardinalOrderClassNames();
+		},
+
+		/**
+		 * Update ordering of widget control forms when the setting is updated
+		 */
+		_setupModel: function() {
+			var control = this;
+			var registered_sidebar = self.registered_sidebars.get( control.params.sidebar_id );
+
+			control.setting.bind( function( new_widget_ids, old_widget_ids ) {
+				var removed_widget_ids = _( old_widget_ids ).difference( new_widget_ids );
+
+				// Filter out any persistent widget_ids for widgets which have been deactivated
+				new_widget_ids = _( new_widget_ids ).filter( function ( new_widget_id ) {
+					var parsed_widget_id = parse_widget_id( new_widget_id );
+					return !! self.available_widgets.findWhere( { id_base: parsed_widget_id.id_base } );
+				} );
+
+				var widget_form_controls = _( new_widget_ids ).map( function ( widget_id ) {
+					var widget_form_control = self.getWidgetFormControlForWidget( widget_id );
+					if ( ! widget_form_control ) {
+						widget_form_control = control.addWidget( widget_id );
+					}
+					return widget_form_control;
+				} );
+
+				// Sort widget controls to their new positions
+				widget_form_controls.sort( function ( a, b ) {
+					var a_index = new_widget_ids.indexOf( a.params.widget_id );
+					var b_index = new_widget_ids.indexOf( b.params.widget_id );
+					if ( a_index === b_index ) {
+						return 0;
+					}
+					return a_index < b_index ? -1 : 1;
+				} );
+
+				var sidebar_widgets_add_control = control.section_content.find( '.customize-control-sidebar_widgets' );
+
+				// Append the controls to put them in the right order
+				var final_control_containers = _( widget_form_controls ).map( function( widget_form_controls ) {
+					return widget_form_controls.container[0];
+				} );
+
+				// Re-sort widget form controls (including widgets form other sidebars newly moved here)
+				sidebar_widgets_add_control.before( final_control_containers );
+				control._applyCardinalOrderClassNames();
+
+				// If the widget was dragged into the sidebar, make sure the sidebar_id param is updated
+				_( widget_form_controls ).each( function ( widget_form_control ) {
+					widget_form_control.params.sidebar_id = control.params.sidebar_id;
+				} );
+
+				// Cleanup after widget removal
+				_( removed_widget_ids ).each( function ( removed_widget_id ) {
+
+					// Using setTimeout so that when moving a widget to another sidebar, the other sidebars_widgets settings get a chance to update
+					setTimeout( function () {
+						var is_present_in_another_sidebar = false;
+
+						// Check if the widget is in another sidebar
+						wp.customize.each( function ( other_setting ) {
+							if ( other_setting.id === control.setting.id || 0 !== other_setting.id.indexOf( 'sidebars_widgets[' ) || other_setting.id === 'sidebars_widgets[wp_inactive_widgets]' ) {
+								return;
+							}
+							var other_sidebar_widgets = other_setting();
+							var i = other_sidebar_widgets.indexOf( removed_widget_id );
+							if ( -1 !== i ) {
+								is_present_in_another_sidebar = true;
+							}
+						} );
+
+						// If the widget is present in another sidebar, abort!
+						if ( is_present_in_another_sidebar ) {
+							return;
+						}
+
+						var removed_control = self.getWidgetFormControlForWidget( removed_widget_id );
+
+						// Detect if widget control was dragged to another sidebar
+						var was_dragged_to_another_sidebar = (
+							removed_control &&
+							$.contains( document, removed_control.container[0] ) &&
+							! $.contains( control.section_content[0], removed_control.container[0] )
+						);
+
+						// Delete any widget form controls for removed widgets
+						if ( removed_control && ! was_dragged_to_another_sidebar ) {
+							wp.customize.control.remove( removed_control.id );
+							removed_control.container.remove();
+						}
+
+						// Move widget to inactive widgets sidebar (move it to trash) if has been previously saved
+						// This prevents the inactive widgets sidebar from overflowing with throwaway widgets
+						if ( self.saved_widget_ids[removed_widget_id] ) {
+							var inactive_widgets = wp.customize.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
+							inactive_widgets.push( removed_widget_id );
+							wp.customize.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactive_widgets ).unique() );
+						}
+
+						// Make old single widget available for adding again
+						var removed_id_base = parse_widget_id( removed_widget_id ).id_base;
+						var widget = self.available_widgets.findWhere( { id_base: removed_id_base } );
+						if ( widget && ! widget.get( 'is_multi' ) ) {
+							widget.set( 'is_disabled', false );
+						}
+					} );
+
+				} );
+			} );
+
+			// Update the model with whether or not the sidebar is rendered
+			self.previewer.bind( 'rendered-sidebars', function ( rendered_sidebars ) {
+				var is_rendered = !! rendered_sidebars[control.params.sidebar_id];
+				registered_sidebar.set( 'is_rendered', is_rendered );
+			} );
+
+			// Show the sidebar section when it becomes visible
+			registered_sidebar.on( 'change:is_rendered', function ( ) {
+				var section_selector = '#accordion-section-sidebar-widgets-' + this.get( 'id' );
+				var section = $( section_selector );
+				if ( this.get( 'is_rendered' ) ) {
+					section.stop().slideDown( function () {
+						$( this ).css( 'height', 'auto' ); // so that the .accordion-section-content won't overflow
+					} );
+				} else {
+					// Make sure that hidden sections get closed first
+					if ( section.hasClass( 'open' ) ) {
+						// it would be nice if accordionSwitch() in accordion.js was public
+						section.find( '.accordion-section-title' ).trigger( 'click' );
+					}
+					section.stop().slideUp();
+				}
+			} );
+		},
+
+		/**
+		 * Allow widgets in sidebar to be re-ordered, and for the order to be previewed
+		 */
+		_setupSortable: function () {
+			var control = this;
+			control.is_reordering = false;
+
+			/**
+			 * Update widget order setting when controls are re-ordered
+			 */
+			control.section_content.sortable( {
+				items: '> .customize-control-widget_form',
+				handle: '.widget-top',
+				axis: 'y',
+				connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
+				update: function () {
+					var widget_container_ids = control.section_content.sortable( 'toArray' );
+					var widget_ids = $.map( widget_container_ids, function ( widget_container_id ) {
+						return $( '#' + widget_container_id ).find( ':input[name=widget-id]' ).val();
+					} );
+					control.setting( widget_ids );
+				}
+			} );
+
+			/**
+			 * Expand other customizer sidebar section when dragging a control widget over it,
+			 * allowing the control to be dropped into another section
+			 */
+			control.control_section.find( '.accordion-section-title' ).droppable( {
+				accept: '.customize-control-widget_form',
+				over: function () {
+					if ( ! control.control_section.hasClass( 'open' ) ) {
+						control.control_section.addClass( 'open' );
+						control.section_content.toggle( false ).slideToggle( 150, function () {
+							control.section_content.sortable( 'refreshPositions' );
+						} );
+					}
+				}
+			} );
+
+			/**
+			 * Keyboard-accessible reordering
+			 */
+			control.container.find( '.reorder-toggle' ).on( 'click keydown', function( event ) {
+				if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar
+					return;
+				}
+
+				control.toggleReordering( ! control.is_reordering );
+			} );
+		},
+
+		/**
+		 * Set up UI for adding a new widget
+		 */
+		_setupAddition: function () {
+			var control = this;
+
+			control.container.find( '.add-new-widget' ).on( 'click keydown', function( event ) {
+				if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar
+					return;
+				}
+
+				if ( control.section_content.hasClass( 'reordering' ) ) {
+					return;
+				}
+
+				// @todo Use an control.is_adding state
+				if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
+					self.availableWidgetsPanel.open( control );
+				} else {
+					self.availableWidgetsPanel.close();
+				}
+			} );
+		},
+
+		/**
+		 * Add classes to the widget_form controls to assist with styling
+		 */
+		_applyCardinalOrderClassNames: function () {
+			var control = this;
+			control.section_content.find( '.customize-control-widget_form' )
+				.removeClass( 'first-widget' )
+				.removeClass( 'last-widget' )
+				.find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
+
+			control.section_content.find( '.customize-control-widget_form:first' )
+				.addClass( 'first-widget' )
+				.find( '.move-widget-up' ).prop( 'tabIndex', -1 );
+			control.section_content.find( '.customize-control-widget_form:last' )
+				.addClass( 'last-widget' )
+				.find( '.move-widget-down' ).prop( 'tabIndex', -1 );
+		},
+
+
+		/***********************************************************************
+		 * Begin public API methods
+		 **********************************************************************/
+
+		/**
+		 * Enable/disable the reordering UI
+		 *
+		 * @param {Boolean} toggle to enable/disable reordering
+		 */
+		toggleReordering: function ( toggle ) {
+			var control = this;
+			toggle = Boolean( toggle );
+			if ( toggle === control.section_content.hasClass( 'reordering' ) ) {
+				return;
+			}
+
+			control.is_reordering = toggle;
+			control.section_content.toggleClass( 'reordering', toggle );
+
+			if ( toggle ) {
+				_( control.getWidgetFormControls() ).each( function ( form_control ) {
+					form_control.collapseForm();
+				} );
+			}
+		},
+
+		/**
+		 * @return {wp.customize.controlConstructor.widget_form[]}
+		 */
+		getWidgetFormControls: function () {
+			var control = this;
+			var form_controls = _( control.setting() ).map( function ( widget_id ) {
+				var setting_id = widget_id_to_setting_id( widget_id );
+				var form_control = customize.control( setting_id );
+				if ( ! form_control ) {
+					throw new Error( 'Unable to find widget_form control for ' + widget_id );
+				}
+				return form_control;
+			} );
+			return form_controls;
+		},
+
+		/**
+		 * @param {string} widget_id or an id_base for adding a previously non-existing widget
+		 * @returns {object} widget_form control instance
+		 */
+		addWidget: function ( widget_id ) {
+			var control = this;
+			var parsed_widget_id = parse_widget_id( widget_id );
+			var widget_number = parsed_widget_id.number;
+			var widget_id_base = parsed_widget_id.id_base;
+			var widget = self.available_widgets.findWhere( {id_base: widget_id_base} );
+			if ( ! widget ) {
+				throw new Error( 'Widget unexpectedly not found.' );
+			}
+			if ( widget_number && ! widget.get( 'is_multi' ) ) {
+				throw new Error( 'Did not expect a widget number to be supplied for a non-multi widget' );
+			}
+
+			// Set up new multi widget
+			if ( widget.get( 'is_multi' ) && ! widget_number ) {
+				widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 );
+				widget_number = widget.get( 'multi_number' );
+			}
+
+			var control_html = $( '#widget-tpl-' + widget.get( 'id' ) ).html();
+			if ( widget.get( 'is_multi' ) ) {
+				control_html = control_html.replace( /<[^<>]+>/g, function ( m ) {
+					return m.replace( /__i__|%i%/g, widget_number );
+				} );
+			} else {
+				widget.set( 'is_disabled', true ); // Prevent single widget from being added again now
+			}
+
+			var customize_control_type = 'widget_form';
+			var customize_control = $( '<li></li>' );
+			customize_control.addClass( 'customize-control' );
+			customize_control.addClass( 'customize-control-' + customize_control_type );
+			customize_control.append( $( control_html ) );
+			customize_control.find( '> .widget-icon' ).remove();
+			if ( widget.get( 'is_multi' ) ) {
+				customize_control.find( 'input[name="widget_number"]' ).val( widget_number );
+				customize_control.find( 'input[name="multi_number"]' ).val( widget_number );
+			}
+			widget_id = customize_control.find( '[name="widget-id"]' ).val();
+			customize_control.hide(); // to be slid-down below
+
+			var setting_id = 'widget_' + widget.get( 'id_base' );
+			if ( widget.get( 'is_multi' ) ) {
+				setting_id += '[' + widget_number + ']';
+			}
+			customize_control.attr( 'id', 'customize-control-' + setting_id.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
+
+			control.container.after( customize_control );
+
+			// Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
+			var is_existing_widget = wp.customize.has( setting_id );
+			if ( ! is_existing_widget ) {
+				var setting_args = {
+					transport: 'refresh',
+					previewer: control.setting.previewer
+				};
+				wp.customize.create( setting_id, setting_id, {}, setting_args );
+			}
+
+			var Constructor = wp.customize.controlConstructor[customize_control_type];
+			var widget_form_control = new Constructor( setting_id, {
+				params: {
+					settings: {
+						'default': setting_id
+					},
+					sidebar_id: control.params.sidebar_id,
+					widget_id: widget_id,
+					widget_id_base: widget.get( 'id_base' ),
+					type: customize_control_type,
+					is_new: ! is_existing_widget,
+					width: widget.get( 'width' ),
+					height: widget.get( 'height' ),
+					is_wide: widget.get( 'is_wide' )
+				},
+				previewer: control.setting.previewer
+			} );
+			wp.customize.control.add( setting_id, widget_form_control );
+
+			// Make sure widget is removed from the other sidebars
+			wp.customize.each( function ( other_setting ) {
+				if ( other_setting.id === control.setting.id ) {
+					return;
+				}
+				if ( 0 !== other_setting.id.indexOf( 'sidebars_widgets[' ) ) {
+					return;
+				}
+				var other_sidebar_widgets = other_setting().slice();
+				var i = other_sidebar_widgets.indexOf( widget_id );
+				if ( -1 !== i ) {
+					other_sidebar_widgets.splice( i );
+					other_setting( other_sidebar_widgets );
+				}
+			} );
+
+			// Add widget to this sidebar
+			var sidebar_widgets = control.setting().slice();
+			if ( -1 === sidebar_widgets.indexOf( widget_id ) ) {
+				sidebar_widgets.push( widget_id );
+				control.setting( sidebar_widgets );
+			}
+
+			customize_control.slideDown( function () {
+				if ( is_existing_widget ) {
+					widget_form_control.expandForm();
+					widget_form_control.updateWidget( {
+						instance: widget_form_control.setting(),
+						complete: function ( error ) {
+							if ( error ) {
+								throw error;
+							}
+							widget_form_control.focus();
+						}
+					} );
+				} else {
+					widget_form_control.focus();
+				}
+			} );
+
+			return widget_form_control;
+		}
+
+	} );
+
+	/**
+	 * Widget Form control
+	 * Note that 'widget_form' must match the Widget_Form_WP_Customize_Control::$type
+	 */
+	customize.controlConstructor.widget_form = customize.Control.extend( {
+
+		/**
+		 * Set up the control
+		 */
+		ready: function() {
+			var control = this;
+			control._setupModel();
+			control._setupWideWidget();
+			control._setupControlToggle();
+			control._setupWidgetTitle();
+			control._setupReorderUI();
+			control._setupHighlightEffects();
+			control._setupUpdateUI();
+			control._setupRemoveUI();
+			control.hook( 'init' );
+		},
+
+		/**
+		 * Hooks for widgets to support living in the customizer control
+		 */
+		hooks: {
+			_default: {},
+			rss: {
+				formUpdated: function ( serialized_form ) {
+					var control = this;
+					var old_widget_error = control.container.find( '.widget-error:first' );
+					var new_widget_error = serialized_form.find( '.widget-error:first' );
+					if ( old_widget_error.length && new_widget_error.length ) {
+						old_widget_error.replaceWith( new_widget_error );
+					} else if ( old_widget_error.length ) {
+						old_widget_error.remove();
+					} else if ( new_widget_error.length ) {
+						control.container.find( '.widget-content' ).prepend( new_widget_error );
+					}
+				}
+			}
+		},
+
+		/**
+		 * Trigger an 'action' which a specific widget type can handle
+		 *
+		 * @param name
+		 */
+		hook: function ( name ) {
+			var args = Array.prototype.slice.call( arguments, 1 );
+			var handler;
+			if ( this.hooks[this.params.widget_id_base] && this.hooks[this.params.widget_id_base][name] ) {
+				handler = this.hooks[this.params.widget_id_base][name];
+			} else if ( this.hooks._default[name] ) {
+				handler = this.hooks._default[name];
+			}
+			if ( handler ) {
+				handler.apply( this, args );
+			}
+		},
+
+		/**
+		 * Handle changes to the setting
+		 */
+		_setupModel: function () {
+			var control = this;
+
+			// Remember saved widgets so we know which to trash (move to inactive widgets sidebar)
+			var remember_saved_widget_id = function () {
+				self.saved_widget_ids[control.params.widget_id] = true;
+			};
+			wp.customize.bind( 'ready', remember_saved_widget_id );
+			wp.customize.bind( 'saved', remember_saved_widget_id );
+
+			control._update_count = 0;
+			control.is_widget_updating = false;
+
+			// Update widget whenever model changes
+			control.setting.bind( function( to, from ) {
+				if ( ! _( from ).isEqual( to ) && ! control.is_widget_updating ) {
+					control.updateWidget( { instance: to } );
+				}
+			} );
+		},
+
+		/**
+		 * Add special behaviors for wide widget controls
+		 */
+		_setupWideWidget: function () {
+			var control = this;
+			if ( ! control.params.is_wide ) {
+				return;
+			}
+			var widget_inside = control.container.find( '.widget-inside' );
+			var customize_sidebar = $( '.wp-full-overlay-sidebar-content:first' );
+			control.container.addClass( 'wide-widget-control' );
+
+			control.container.find( '.widget-content:first' ).css( {
+				'min-width': control.params.width,
+				'min-height': control.params.height
+			} );
+
+			/**
+			 * Keep the widget-inside positioned so the top of fixed-positioned
+			 * element is at the same top position as the widget-top. When the
+			 * widget-top is scrolled out of view, keep the widget-top in view;
+			 * likewise, don't allow the widget to drop off the bottom of the window.
+			 */
+			var position_widget = function () {
+				var offset_top = control.container.offset().top;
+				var height = widget_inside.outerHeight();
+				var top = Math.max( offset_top, 0 );
+				var max_top = $( window ).height() - height;
+				top = Math.min( top, max_top );
+				widget_inside.css( 'top', top );
+			};
+
+			var theme_controls_container = $( '#customize-theme-controls' );
+			control.container.on( 'expand', function () {
+				customize_sidebar.on( 'scroll', position_widget );
+				$( window ).on( 'resize', position_widget );
+				theme_controls_container.on( 'expanded collapsed', position_widget );
+				position_widget();
+			} );
+			control.container.on( 'collapsed', function () {
+				customize_sidebar.off( 'scroll', position_widget );
+				theme_controls_container.off( 'expanded collapsed', position_widget );
+				$( window ).off( 'resize', position_widget );
+			} );
+
+			// Reposition whenever a sidebar's widgets are changed
+			wp.customize.each( function ( setting ) {
+				if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
+					setting.bind( function () {
+						if ( control.container.hasClass( 'expanded' ) ) {
+							position_widget();
+						}
+					} );
+				}
+			} );
+		},
+
+		/**
+		 * Show/hide the control when clicking on the form title, when clicking
+		 * the close button
+		 */
+		_setupControlToggle: function() {
+			var control = this;
+			control.container.find( '.widget-top' ).on( 'click', function ( e ) {
+				e.preventDefault();
+				var sidebar_widgets_control = control.getSidebarWidgetsControl();
+				if ( sidebar_widgets_control.is_reordering ) {
+					return;
+				}
+				control.toggleForm();
+			} );
+
+			var close_btn = control.container.find( '.widget-control-close' );
+			// @todo Hitting Enter on this link does nothing; will be resolved in core with <http://core.trac.wordpress.org/ticket/26633>
+			close_btn.on( 'click', function ( e ) {
+				e.preventDefault();
+				control.collapseForm();
+				control.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
+			} );
+		},
+
+		/**
+		 * Update the title of the form if a title field is entered
+		 */
+		_setupWidgetTitle: function () {
+			var control = this;
+			var update_title = function () {
+				var title = control.setting().title;
+				var in_widget_title = control.container.find( '.in-widget-title' );
+				if ( title ) {
+					in_widget_title.text( ': ' + title );
+				} else {
+					in_widget_title.text( '' );
+				}
+			};
+			control.setting.bind( update_title );
+			update_title();
+		},
+
+		/**
+		 * Set up the widget-reorder-nav
+		 */
+		_setupReorderUI: function () {
+			var control = this;
+
+			/**
+			 * select the provided sidebar list item in the move widget area
+			 *
+			 * @param {jQuery} li
+			 */
+			var select_sidebar_item = function ( li ) {
+				li.siblings( '.selected' ).removeClass( 'selected' );
+				li.addClass( 'selected' );
+				var is_self_sidebar = ( li.data( 'id' ) === control.params.sidebar_id );
+				control.container.find( '.move-widget-btn' ).prop( 'disabled', is_self_sidebar );
+			};
+
+			/**
+			 * Add the widget reordering elements to the widget control
+			 */
+			control.container.find( '.widget-title-action' ).after( $( self.tpl.widget_reorder_nav ) );
+			var move_widget_area = $(
+				_.template( self.tpl.move_widget_area, {
+					sidebars: _( self.registered_sidebars.toArray() ).pluck( 'attributes' )
+				} )
+			);
+			control.container.find( '.widget-top' ).after( move_widget_area );
+
+			/**
+			 * Update available sidebars when their rendered state changes
+			 */
+			var update_available_sidebars = function () {
+				var sidebar_items = move_widget_area.find( 'li' );
+				var self_sidebar_item = sidebar_items.filter( function(){
+					return $( this ).data( 'id' ) === control.params.sidebar_id;
+				} );
+				sidebar_items.each( function () {
+					var li = $( this );
+					var sidebar_id = li.data( 'id' );
+					var sidebar_model = self.registered_sidebars.get( sidebar_id );
+					li.toggle( sidebar_model.get( 'is_rendered' ) );
+					if ( li.hasClass( 'selected' ) && ! sidebar_model.get( 'is_rendered' ) ) {
+						select_sidebar_item( self_sidebar_item );
+					}
+				} );
+			};
+			update_available_sidebars();
+			self.registered_sidebars.on( 'change:is_rendered', update_available_sidebars );
+
+			/**
+			 * Handle clicks for up/down/move on the reorder nav
+			 */
+			var reorder_nav = control.container.find( '.widget-reorder-nav' );
+			reorder_nav.find( '.move-widget, .move-widget-down, .move-widget-up' ).on( 'click keypress', function ( event ) {
+				if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
+					return;
+				}
+				$( this ).focus();
+
+				if ( $( this ).is( '.move-widget' ) ) {
+					control.toggleWidgetMoveArea();
+				} else {
+					var is_move_down = $( this ).is( '.move-widget-down' );
+					var is_move_up = $( this ).is( '.move-widget-up' );
+					var i = control.getWidgetSidebarPosition();
+					if ( ( is_move_up && i === 0 ) || ( is_move_down && i === control.getSidebarWidgetsControl().setting().length - 1 ) ) {
+						return;
+					}
+
+					if ( is_move_up ) {
+						control.moveUp();
+					} else {
+						control.moveDown();
+					}
+
+					$( this ).focus(); // re-focus after the container was moved
+				}
+			} );
+
+			/**
+			 * Handle selecting a sidebar to move to
+			 */
+			control.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function ( e ) {
+				if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
+					return;
+				}
+				e.preventDefault();
+				select_sidebar_item( $( this ) );
+			} );
+
+			/**
+			 * Move widget to another sidebar
+			 */
+			control.container.find( '.move-widget-btn' ).click( function () {
+				control.getSidebarWidgetsControl().toggleReordering( false );
+
+				var old_sidebar_id = control.params.sidebar_id;
+				var new_sidebar_id = control.container.find( '.widget-area-select li.selected' ).data( 'id' );
+				var old_sidebar_widgets_setting = customize( 'sidebars_widgets[' + old_sidebar_id + ']' );
+				var new_sidebar_widgets_setting = customize( 'sidebars_widgets[' + new_sidebar_id + ']' );
+				var old_sidebar_widget_ids = Array.prototype.slice.call( old_sidebar_widgets_setting() );
+				var new_sidebar_widget_ids = Array.prototype.slice.call( new_sidebar_widgets_setting() );
+
+				var i = control.getWidgetSidebarPosition();
+				old_sidebar_widget_ids.splice( i, 1 );
+				new_sidebar_widget_ids.push( control.params.widget_id );
+
+				old_sidebar_widgets_setting( old_sidebar_widget_ids );
+				new_sidebar_widgets_setting( new_sidebar_widget_ids );
+
+				control.focus();
+			} );
+		},
+
+		/**
+		 * Highlight widgets in preview when interacted with in the customizer
+		 */
+		_setupHighlightEffects: function() {
+			var control = this;
+
+			// Highlight whenever hovering or clicking over the form
+			control.container.on( 'mouseenter click', function () {
+				control.highlightPreviewWidget();
+			} );
+
+			// Highlight when the setting is updated
+			control.setting.bind( function () {
+				control.scrollPreviewWidgetIntoView();
+				control.highlightPreviewWidget();
+			} );
+
+			// Highlight when the widget form is expanded
+			control.container.on( 'expand', function () {
+				control.scrollPreviewWidgetIntoView();
+			} );
+		},
+
+		/**
+		 * Set up event handlers for widget updating
+		 */
+		_setupUpdateUI: function () {
+			var control = this;
+
+			var widget_content = control.container.find( '.widget-content' );
+
+			// Configure update button
+			var save_btn = control.container.find( '.widget-control-save' );
+			save_btn.val( self.i18n.save_btn_label );
+			save_btn.attr( 'title', self.i18n.save_btn_tooltip );
+			save_btn.removeClass( 'button-primary' ).addClass( 'button-secondary' );
+			save_btn.on( 'click', function ( e ) {
+				e.preventDefault();
+				control.updateWidget();
+			} );
+
+			var trigger_save = _.debounce( function () {
+				// @todo For compatibility with other plugins, should we trigger a click event? What about form submit event?
+				control.updateWidget();
+			}, 250 );
+
+			// Trigger widget form update when hitting Enter within an input
+			control.container.find( '.widget-content' ).on( 'keydown', 'input', function( e ) {
+				if ( 13 === e.which ) { // Enter
+					e.preventDefault();
+					control.updateWidget( { ignore_active_element: true } );
+				}
+			} );
+
+			// Handle widgets that support live previews
+			widget_content.on( 'change input propertychange', ':input', function ( e ) {
+				if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
+					trigger_save();
+				}
+			} );
+
+			// Remove loading indicators when the setting is saved and the preview updates
+			control.setting.previewer.channel.bind( 'synced', function () {
+				control.container.removeClass( 'previewer-loading' );
+			} );
+			self.previewer.bind( 'widget-updated', function ( updated_widget_id ) {
+				if ( updated_widget_id === control.params.widget_id ) {
+					control.container.removeClass( 'previewer-loading' );
+				}
+			} );
+
+			// Update widget control to indicate whether it is currently rendered (cf. Widget Visibility)
+			self.previewer.bind( 'rendered-widgets', function ( rendered_widgets ) {
+				var is_rendered = !! rendered_widgets[control.params.widget_id];
+				control.container.toggleClass( 'widget-rendered', is_rendered );
+			} );
+		},
+
+		/**
+		 * Set up event handlers for widget removal
+		 */
+		_setupRemoveUI: function () {
+			var control = this;
+
+			// Configure remove button
+			var remove_btn = control.container.find( 'a.widget-control-remove' );
+			// @todo Hitting Enter on this link does nothing; will be resolved in core with <http://core.trac.wordpress.org/ticket/26633>
+			remove_btn.on( 'click', function ( e ) {
+				e.preventDefault();
+
+				// Find an adjacent element to add focus to when this widget goes away
+				var adjacent_focus_target;
+				if ( control.container.next().is( '.customize-control-widget_form' ) ) {
+					adjacent_focus_target = control.container.next().find( '.widget-action:first' );
+				} else if ( control.container.prev().is( '.customize-control-widget_form' ) ) {
+					adjacent_focus_target = control.container.prev().find( '.widget-action:first' );
+				} else {
+					adjacent_focus_target = control.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );
+				}
+
+				control.container.slideUp( function() {
+					var sidebars_widgets_control = self.getSidebarWidgetControlContainingWidget( control.params.widget_id );
+					if ( ! sidebars_widgets_control ) {
+						throw new Error( 'Unable to find sidebars_widgets_control' );
+					}
+					var sidebar_widget_ids = sidebars_widgets_control.setting().slice();
+					var i = sidebar_widget_ids.indexOf( control.params.widget_id );
+					if ( -1 === i ) {
+						throw new Error( 'Widget is not in sidebar' );
+					}
+					sidebar_widget_ids.splice( i, 1 );
+					sidebars_widgets_control.setting( sidebar_widget_ids );
+					adjacent_focus_target.focus(); // keyboard accessibility
+				} );
+			} );
+
+			var replace_delete_with_remove = function () {
+				remove_btn.text( self.i18n.remove_btn_label ); // wp_widget_control() outputs the link as "Delete"
+				remove_btn.attr( 'title', self.i18n.remove_btn_tooltip );
+			};
+			if ( control.params.is_new ) {
+				wp.customize.bind( 'saved', replace_delete_with_remove );
+			} else {
+				replace_delete_with_remove();
+			}
+		},
+
+		/**
+		 * Iterate over supplied inputs and create a signature string for all of them together.
+		 * This string can be used to compare whether or not the form has all of the same fields.
+		 *
+		 * @param {jQuery} inputs
+		 * @returns {string}
+		 * @private
+		 */
+		_getInputsSignature: function ( inputs ) {
+			var inputs_signatures = _( inputs ).map( function ( input ) {
+				input = $( input );
+				var signature_parts;
+				if ( input.is( 'option' ) ) {
+					signature_parts = [ input.prop( 'nodeName' ), input.prop( 'value' ) ];
+				} else if ( input.is( ':checkbox, :radio' ) ) {
+					signature_parts = [ input.prop( 'type' ), input.attr( 'id' ), input.attr( 'name' ), input.prop( 'value' ) ];
+				} else {
+					signature_parts = [ input.prop( 'nodeName' ), input.attr( 'id' ), input.attr( 'name' ), input.attr( 'type' ) ];
+				}
+				return signature_parts.join( ',' );
+			} );
+			return inputs_signatures.join( ';' );
+		},
+
+		/**
+		 * Get the property that represents the state of an input.
+		 *
+		 * @param {jQuery|DOMElement} input
+		 * @returns {string}
+		 * @private
+		 */
+		_getInputStatePropertyName: function ( input ) {
+			input = $( input );
+			if ( input.is( ':radio, :checkbox' ) ) {
+				return 'checked';
+			} else if ( input.is( 'option' ) ) {
+				return 'selected';
+			} else {
+				return 'value';
+			}
+		},
+
+		/***********************************************************************
+		 * Begin public API methods
+		 **********************************************************************/
+
+		/**
+		 * @return {wp.customize.controlConstructor.sidebar_widgets[]}
+		 */
+		getSidebarWidgetsControl: function () {
+			var control = this;
+			var setting_id = 'sidebars_widgets[' + control.params.sidebar_id + ']';
+			var sidebar_widgets_control = customize.control( setting_id );
+			if ( ! sidebar_widgets_control ) {
+				throw new Error( 'Unable to locate sidebar_widgets control for ' + control.params.sidebar_id );
+			}
+			return sidebar_widgets_control;
+		},
+
+		/**
+		 * Submit the widget form via Ajax and get back the updated instance,
+		 * along with the new widget control form to render.
+		 *
+		 * @param {object} [args]
+		 * @param {Object|null} [args.instance=null]  When the model changes, the instance is sent here; otherwise, the inputs from the form are used
+		 * @param {Function|null} [args.complete=null]  Function which is called when the request finishes. Context is bound to the control. First argument is any error. Following arguments are for success.
+		 * @param {Boolean} [args.ignore_active_element=false] Whether or not updating a field will be deferred if focus is still on the element.
+		 */
+		updateWidget: function ( args ) {
+			var control = this;
+			args = $.extend( {
+				instance: null,
+				complete: null,
+				ignore_active_element: false
+			}, args );
+			var instance_override = args.instance;
+			var complete_callback = args.complete;
+
+			control._update_count += 1;
+			var update_number = control._update_count;
+
+			var widget_content = control.container.find( '.widget-content' );
+
+			var element_id_to_refocus = null;
+			var active_input_selection_start = null;
+			var active_input_selection_end = null;
+			// @todo Support more selectors than IDs?
+			if ( $.contains( control.container[0], document.activeElement ) && $( document.activeElement ).is( '[id]' ) ) {
+				element_id_to_refocus = $( document.activeElement ).prop( 'id' );
+				// @todo IE8 support: http://stackoverflow.com/a/4207763/93579
+				try {
+					active_input_selection_start = document.activeElement.selectionStart;
+					active_input_selection_end = document.activeElement.selectionEnd;
+				}
+				catch( e ) {} // catch InvalidStateError in case of checkboxes
+			}
+
+			control.container.addClass( 'widget-form-loading' );
+			control.container.addClass( 'previewer-loading' );
+
+			var params = {};
+			params.action = self.update_widget_ajax_action;
+			params[self.update_widget_nonce_post_key] = self.update_widget_nonce_value;
+
+			var data = $.param( params );
+			var inputs = widget_content.find( ':input, option' );
+
+			// Store the value we're submitting in data so that when the response comes back,
+			// we know if it got sanitized; if there is no difference in the sanitized value,
+			// then we do not need to touch the UI and mess up the user's ongoing editing.
+			inputs.each( function () {
+				var input = $( this );
+				var property = control._getInputStatePropertyName( this );
+				input.data( 'state' + update_number, input.prop( property ) );
+			} );
+
+			if ( instance_override ) {
+				data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instance_override ) } );
+			} else {
+				data += '&' + inputs.serialize();
+			}
+			data += '&' + widget_content.find( '~ :input' ).serialize();
+
+			console.log( wp.ajax.settings.url, data );
+			var jqxhr = $.post( wp.ajax.settings.url, data, function ( r ) {
+				if ( r.success ) {
+					var sanitized_form = $( '<div>' + r.data.form + '</div>' );
+					control.hook( 'formUpdate', sanitized_form );
+
+					var sanitized_inputs = sanitized_form.find( ':input, option' );
+					var has_same_inputs_in_response = control._getInputsSignature( inputs ) === control._getInputsSignature( sanitized_inputs );
+
+					if ( has_same_inputs_in_response ) {
+						inputs.each( function ( i ) {
+							var input = $( this );
+							var sanitized_input = $( sanitized_inputs[i] );
+							var property = control._getInputStatePropertyName( this );
+							var state = input.data( 'state' + update_number );
+							var sanitized_state = sanitized_input.prop( property );
+							input.data( 'sanitized', sanitized_state );
+
+							if ( state !== sanitized_state ) {
+
+								// Only update now if not currently focused on it,
+								// so that we don't cause the cursor
+								// it will be updated upon the change event
+								if ( args.ignore_active_element || ! input.is( document.activeElement ) ) {
+									input.prop( property, sanitized_state );
+								}
+								control.hook( 'unsanitaryField', input, sanitized_state, state );
+
+							} else {
+								control.hook( 'sanitaryField', input, state );
+							}
+						} );
+						control.hook( 'formUpdated', sanitized_form );
+					} else {
+						widget_content.html( sanitized_form.html() );
+						if ( element_id_to_refocus ) {
+							// not using jQuery selector so we don't have to worry about escaping IDs with brackets and other characters
+							$( document.getElementById( element_id_to_refocus ) )
+								.prop( {
+									selectionStart: active_input_selection_start,
+									selectionEnd: active_input_selection_end
+								} )
+								.focus();
+						}
+						control.hook( 'formRefreshed' );
+					}
+
+					/**
+					 * If the old instance is identical to the new one, there is nothing new
+					 * needing to be rendered, and so we can preempt the event for the
+					 * preview finishing loading.
+					 */
+					var is_instance_identical = _( control.setting() ).isEqual( r.data.instance );
+					if ( is_instance_identical ) {
+						control.container.removeClass( 'previewer-loading' );
+					} else {
+						control.is_widget_updating = true; // suppress triggering another updateWidget
+						control.setting( r.data.instance );
+						control.is_widget_updating = false;
+					}
+
+					if ( complete_callback ) {
+						complete_callback.call( control, null, { no_change: is_instance_identical, ajax_finished: true } );
+					}
+				} else {
+					console.log( r );
+					var message = 'FAIL';
+					if ( r.data && r.data.message ) {
+						message = r.data.message;
+					}
+					if ( complete_callback ) {
+						complete_callback.call( control, message );
+					} else {
+						throw new Error( message );
+					}
+				}
+			} );
+			jqxhr.fail( function ( jqXHR, textStatus ) {
+				if ( complete_callback ) {
+					complete_callback.call( control, textStatus );
+				} else {
+					throw new Error( textStatus );
+				}
+			} );
+			jqxhr.always( function () {
+				control.container.removeClass( 'widget-form-loading' );
+				inputs.each( function () {
+					$( this ).removeData( 'state' + update_number );
+				} );
+			} );
+		},
+
+		/**
+		 * Expand the accordion section containing a control
+		 * @todo it would be nice if accordion had a proper API instead of having to trigger UI events on its elements
+		 */
+		expandControlSection: function () {
+			var section = this.container.closest( '.accordion-section' );
+			if ( ! section.hasClass( 'open' ) ) {
+				section.find( '.accordion-section-title:first' ).trigger( 'click' );
+			}
+		},
+
+		/**
+		 * Expand the widget form control
+		 */
+		expandForm: function () {
+			this.toggleForm( true );
+		},
+
+		/**
+		 * Collapse the widget form control
+		 */
+		collapseForm: function () {
+			this.toggleForm( false );
+		},
+
+		/**
+		 * Expand or collapse the widget control
+		 *
+		 * @param {boolean|undefined} [do_expand] If not supplied, will be inverse of current visibility
+		 */
+		toggleForm: function ( do_expand ) {
+			var control = this;
+			var widget = control.container.find( 'div.widget:first' );
+			var inside = widget.find( '.widget-inside:first' );
+			if ( typeof do_expand === 'undefined' ) {
+				do_expand = ! inside.is( ':visible' );
+			}
+
+			// Already expanded or collapsed, so noop
+			if ( inside.is( ':visible' ) === do_expand ) {
+				return;
+			}
+
+			var complete;
+			if ( do_expand ) {
+				// Close all other widget controls before expanding this one
+				wp.customize.control.each( function ( other_control ) {
+					if ( control.params.type === other_control.params.type && control !== other_control ) {
+						other_control.collapseForm();
+					}
+				} );
+
+				control.container.trigger( 'expand' );
+				control.container.addClass( 'expanding' );
+				complete = function () {
+					control.container.removeClass( 'expanding' );
+					control.container.addClass( 'expanded' );
+					control.container.trigger( 'expanded' );
+				};
+				if ( control.params.is_wide ) {
+					inside.animate( { width: 'show' }, 'fast', complete );
+				} else {
+					inside.slideDown( 'fast', complete );
+				}
+			} else {
+				control.container.trigger( 'collapse' );
+				control.container.addClass( 'collapsing' );
+				complete = function () {
+					control.container.removeClass( 'collapsing' );
+					control.container.removeClass( 'expanded' );
+					control.container.trigger( 'collapsed' );
+				};
+				if ( control.params.is_wide ) {
+					inside.animate( { width: 'hide' }, 'fast', complete );
+				} else {
+					inside.slideUp( 'fast', function() {
+						widget.css( { width:'', margin:'' } );
+						complete();
+					} );
+				}
+			}
+		},
+
+		/**
+		 * Expand the containing sidebar section, expand the form, and focus on
+		 * the first input in the control
+		 */
+		focus: function () {
+			var control = this;
+			control.expandControlSection();
+			control.expandForm();
+			control.container.find( ':focusable:first' ).focus().trigger( 'click' );
+		},
+
+		/**
+		 * Get the position (index) of the widget in the containing sidebar
+		 *
+		 * @throws Error
+		 * @returns {Number}
+		 */
+		getWidgetSidebarPosition: function () {
+			var control = this;
+			var sidebar_widget_ids = control.getSidebarWidgetsControl().setting();
+			var position = sidebar_widget_ids.indexOf( control.params.widget_id );
+			if ( position === -1 ) {
+				throw new Error( 'Widget was unexpectedly not present in the sidebar.' );
+			}
+			return position;
+		},
+
+		/**
+		 * Move widget up one in the sidebar
+		 */
+		moveUp: function () {
+			this._moveWidgetByOne( -1 );
+		},
+
+		/**
+		 * Move widget up one in the sidebar
+		 */
+		moveDown: function () {
+			this._moveWidgetByOne( 1 );
+		},
+
+		/**
+		 * @private
+		 *
+		 * @param {Number} offset 1|-1
+		 */
+		_moveWidgetByOne: function ( offset ) {
+			var control = this;
+			var i = control.getWidgetSidebarPosition();
+
+			var sidebar_widgets_setting = control.getSidebarWidgetsControl().setting;
+			var sidebar_widget_ids = Array.prototype.slice.call( sidebar_widgets_setting() ); // clone
+			var adjacent_widget_id = sidebar_widget_ids[i + offset];
+			sidebar_widget_ids[i + offset] = control.params.widget_id;
+			sidebar_widget_ids[i] = adjacent_widget_id;
+
+			sidebar_widgets_setting( sidebar_widget_ids );
+		},
+
+		/**
+		 * Toggle visibility of the widget move area
+		 *
+		 * @param {Boolean} [toggle]
+		 */
+		toggleWidgetMoveArea: function ( toggle ) {
+			var control = this;
+			var move_widget_area = control.container.find( '.move-widget-area' );
+			if ( typeof toggle === 'undefined' ) {
+				toggle = ! move_widget_area.hasClass( 'active' );
+			}
+			if ( toggle ) {
+				// reset the selected sidebar
+				move_widget_area.find( '.selected' ).removeClass( 'selected' );
+				move_widget_area.find( 'li' ).filter( function () {
+					return $( this ).data( 'id' ) === control.params.sidebar_id;
+				} ).addClass( 'selected' );
+				control.container.find( '.move-widget-btn' ).prop( 'disabled', true );
+			}
+			move_widget_area.toggleClass( 'active', toggle );
+		},
+
+		/**
+		 * Inverse of WidgetCustomizer.getControlInstanceForWidget
+		 * @return {jQuery}
+		 */
+		getPreviewWidgetElement: function () {
+			var control = this;
+			var widget_customizer_preview = self.getPreviewWindow().WidgetCustomizerPreview;
+			return widget_customizer_preview.getSidebarWidgetElement( control.params.sidebar_id, control.params.widget_id );
+		},
+
+		/**
+		 * Inside of the customizer preview, scroll the widget into view
+		 */
+		scrollPreviewWidgetIntoView: function () {
+			// @todo scrollIntoView() provides a robust but very poor experience. Animation is needed. See https://github.com/x-team/wp-widget-customizer/issues/16
+		},
+
+		/**
+		 * Highlight the widget control and section
+		 */
+		highlightSectionAndControl: function() {
+			var control = this;
+			var target_element;
+			if ( control.container.is( ':hidden' ) ) {
+				target_element = control.container.closest( '.control-section' );
+			} else {
+				target_element = control.container;
+			}
+
+			$( '.widget-customizer-highlighted' ).removeClass( 'widget-customizer-highlighted' );
+			target_element.addClass( 'widget-customizer-highlighted' );
+			setTimeout( function () {
+				target_element.removeClass( 'widget-customizer-highlighted' );
+			}, 500 );
+		},
+
+		/**
+		 * Add the widget-customizer-highlighted-widget class to the widget for 500ms
+		 */
+		highlightPreviewWidget: function () {
+			var control = this;
+			var widget_el = control.getPreviewWidgetElement();
+			var root_el = widget_el.closest( 'html' );
+			root_el.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
+			widget_el.addClass( 'widget-customizer-highlighted-widget' );
+			setTimeout( function () {
+				widget_el.removeClass( 'widget-customizer-highlighted-widget' );
+			}, 500 );
+		}
+
+	} );
+
+	/**
+	 * Capture the instance of the Previewer since it is private
+	 */
+	var OldPreviewer = wp.customize.Previewer;
+	wp.customize.Previewer = OldPreviewer.extend( {
+		initialize: function( params, options ) {
+			self.previewer = this;
+			OldPreviewer.prototype.initialize.call( this, params, options );
+			this.bind( 'refresh', this.refresh );
+		}
+	} );
+
+	/**
+	 * Given a widget control, find the sidebar widgets control that contains it.
+	 * @param {string} widget_id
+	 * @return {object|null}
+	 */
+	self.getSidebarWidgetControlContainingWidget = function ( widget_id ) {
+		var found_control = null;
+		// @todo this can use widget_id_to_setting_id(), then pass into wp.customize.control( x ).getSidebarWidgetsControl()
+		wp.customize.control.each( function ( control ) {
+			if ( control.params.type === 'sidebar_widgets' && -1 !== control.setting().indexOf( widget_id ) ) {
+				found_control = control;
+			}
+		} );
+		return found_control;
+	};
+
+	/**
+	 * Given a widget_id for a widget appearing in the preview, get the widget form control associated with it
+	 * @param {string} widget_id
+	 * @return {object|null}
+	 */
+	self.getWidgetFormControlForWidget = function ( widget_id ) {
+		var found_control = null;
+		// @todo We can just use widget_id_to_setting_id() here
+		wp.customize.control.each( function ( control ) {
+			if ( control.params.type === 'widget_form' && control.params.widget_id === widget_id ) {
+				found_control = control;
+			}
+		} );
+		return found_control;
+	};
+
+	/**
+	 * @returns {Window}
+	 */
+	self.getPreviewWindow = function (){
+		return $( '#customize-preview' ).find( 'iframe' ).prop( 'contentWindow' );
+	};
+
+	/**
+	 * Available Widgets Panel
+	 */
+	self.availableWidgetsPanel = {
+		active_sidebar_widgets_control: null,
+		selected_widget_tpl: null,
+		container: null,
+		filter_input: null,
+
+		/**
+		 * Set up event listeners
+		 */
+		setup: function () {
+			var panel = this;
+			panel.container = $( '#available-widgets' );
+			panel.filter_input = $( '#available-widgets-filter' ).find( 'input' );
+
+			var update_available_widgets_list = function () {
+				self.available_widgets.each( function ( widget ) {
+					var widget_tpl = $( '#widget-tpl-' + widget.id );
+					widget_tpl.toggle( ! widget.get( 'is_disabled' ) );
+					if ( widget.get( 'is_disabled' ) && widget_tpl.is( panel.selected_widget_tpl ) ) {
+						panel.selected_widget_tpl = null;
+					}
+				} );
+			};
+
+			self.available_widgets.on( 'change', update_available_widgets_list );
+			update_available_widgets_list();
+
+			// If the available widgets panel is open and the customize controls are
+			// interacted with (i.e. available widgets panel is blurred) then close the
+			// available widgets panel.
+			$( '#customize-controls' ).on( 'click keydown', function ( e ) {
+				var is_add_new_widget_btn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
+				if ( $( 'body' ).hasClass( 'adding-widget' ) && ! is_add_new_widget_btn ) {
+					panel.close();
+				}
+			} );
+
+			// Close the panel if the URL in the preview changes
+			self.previewer.bind( 'url', function () {
+				panel.close();
+			} );
+
+			// Submit a selection when clicked or keypressed
+			panel.container.find( '.widget-tpl' ).on( 'click keypress', function( event ) {
+
+				// Only proceed with keypress if it is Enter or Spacebar
+				if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
+					return;
+				}
+
+				panel.submit( this );
+			} );
+
+			panel.container.liveFilter(
+				'#available-widgets-filter input',
+				'.widget-tpl',
+				{
+					filterChildSelector: '.widget-title h4',
+					after: function () {
+						var filter_val = panel.filter_input.val();
+
+						// Remove a widget from being selected if it is no longer visible
+						if ( panel.selected_widget_tpl && ! panel.selected_widget_tpl.is( ':visible' ) ) {
+							panel.selected_widget_tpl.removeClass( 'selected' );
+							panel.selected_widget_tpl = null;
+						}
+
+						// If a widget was selected but the filter value has been cleared out, clear selection
+						if ( panel.selected_widget_tpl && ! filter_val ) {
+							panel.selected_widget_tpl.removeClass( 'selected' );
+							panel.selected_widget_tpl = null;
+						}
+
+						// If a filter has been entered and a widget hasn't been selected, select the first one shown
+						if ( ! panel.selected_widget_tpl && filter_val ) {
+							var first_visible_widget = panel.container.find( '> .widget-tpl:visible:first' );
+							if ( first_visible_widget.length ) {
+								panel.select( first_visible_widget );
+							}
+						}
+
+					}
+				}
+			);
+
+			// Select a widget when it is focused on
+			panel.container.find( ' > .widget-tpl' ).on( 'focus', function () {
+				panel.select( this );
+			} );
+
+			panel.container.on( 'keydown', function ( event ) {
+				var is_enter = ( event.which === 13 );
+				var is_esc = ( event.which === 27 );
+				var is_down = ( event.which === 40 );
+				var is_up = ( event.which === 38 );
+				var selected_widget_tpl = null;
+				var first_visible_widget = panel.container.find( '> .widget-tpl:visible:first' );
+				var last_visible_widget = panel.container.find( '> .widget-tpl:visible:last' );
+				var is_input_focused = $( event.target ).is( panel.filter_input );
+
+				if ( is_down || is_up ) {
+					if ( is_down ) {
+						if ( is_input_focused ) {
+							selected_widget_tpl = first_visible_widget;
+						} else if ( panel.selected_widget_tpl && panel.selected_widget_tpl.nextAll( '.widget-tpl:visible' ).length !== 0 ) {
+							selected_widget_tpl = panel.selected_widget_tpl.nextAll( '.widget-tpl:visible:first' );
+						}
+					} else if ( is_up ) {
+						if ( is_input_focused ) {
+							selected_widget_tpl = last_visible_widget;
+						} else if ( panel.selected_widget_tpl && panel.selected_widget_tpl.prevAll( '.widget-tpl:visible' ).length !== 0 ) {
+							selected_widget_tpl = panel.selected_widget_tpl.prevAll( '.widget-tpl:visible:first' );
+						}
+					}
+					panel.select( selected_widget_tpl );
+					if ( selected_widget_tpl ) {
+						selected_widget_tpl.focus();
+					} else {
+						panel.filter_input.focus();
+					}
+					return;
+				}
+
+				// If enter pressed but nothing entered, don't do anything
+				if ( is_enter && ! panel.filter_input.val() ) {
+					return;
+				}
+
+				if ( is_enter ) {
+					panel.submit();
+				} else if ( is_esc ) {
+					panel.close( { return_focus: true } );
+				}
+			} );
+		},
+
+		/**
+		 * @param widget_tpl
+		 */
+		select: function ( widget_tpl ) {
+			var panel = this;
+			panel.selected_widget_tpl = $( widget_tpl );
+			panel.selected_widget_tpl.siblings( '.widget-tpl' ).removeClass( 'selected' );
+			panel.selected_widget_tpl.addClass( 'selected' );
+		},
+
+		submit: function ( widget_tpl ) {
+			var panel = this;
+			if ( ! widget_tpl ) {
+				widget_tpl = panel.selected_widget_tpl;
+			}
+			if ( ! widget_tpl || ! panel.active_sidebar_widgets_control ) {
+				return;
+			}
+			panel.select( widget_tpl );
+
+			var widget_id = $( panel.selected_widget_tpl ).data( 'widget-id' );
+			var widget = self.available_widgets.findWhere( {id: widget_id} );
+			if ( ! widget ) {
+				throw new Error( 'Widget unexpectedly not found.' );
+			}
+			panel.active_sidebar_widgets_control.addWidget( widget.get( 'id_base' ) );
+			panel.close();
+		},
+
+		/**
+		 * @param sidebars_widgets_control
+		 */
+		open: function ( sidebars_widgets_control ) {
+			var panel = this;
+			panel.active_sidebar_widgets_control = sidebars_widgets_control;
+
+			// Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens
+			_( sidebars_widgets_control.getWidgetFormControls() ).each( function ( control ) {
+				if ( control.params.is_wide ) {
+					control.collapseForm();
+				}
+			} );
+
+			$( 'body' ).addClass( 'adding-widget' );
+			panel.container.find( '.widget-tpl' ).removeClass( 'selected' );
+			panel.filter_input.focus();
+		},
+
+		/**
+		 * Hide the panel
+		 */
+		close: function ( options ) {
+			var panel = this;
+			options = options || {};
+			if ( options.return_focus && panel.active_sidebar_widgets_control ) {
+				panel.active_sidebar_widgets_control.container.find( '.add-new-widget' ).focus();
+			}
+			panel.active_sidebar_widgets_control = null;
+			panel.selected_widget_tpl = null;
+			$( 'body' ).removeClass( 'adding-widget' );
+			panel.filter_input.val( '' );
+		}
+	};
+
+	/**
+	 * @param {String} widget_id
+	 * @returns {Object}
+	 */
+	function parse_widget_id( widget_id ) {
+		var parsed = {
+			number: null,
+			id_base: null
+		};
+		var matches = widget_id.match( /^(.+)-(\d+)$/ );
+		if ( matches ) {
+			parsed.id_base = matches[1];
+			parsed.number = parseInt( matches[2], 10 );
+		} else {
+			// likely an old single widget
+			parsed.id_base = widget_id;
+		}
+		return parsed;
+	}
+
+	/**
+	 * @param {String} widget_id
+	 * @returns {String} setting_id
+	 */
+	function widget_id_to_setting_id( widget_id ) {
+		var parsed = parse_widget_id( widget_id );
+		var setting_id = 'widget_' + parsed.id_base;
+		if ( parsed.number ) {
+			setting_id += '[' + parsed.number + ']';
+		}
+		return setting_id;
+	}
+
+	return self;
+}( jQuery ));
+
+/* @todo remove this dependency */
+/*
+ * jQuery.liveFilter
+ *
+ * Copyright (c) 2009 Mike Merritt
+ *
+ * Forked by Lim Chee Aun (cheeaun.com)
+ *
+ */
+
+(function($){
+	$.fn.liveFilter = function(inputEl, filterEl, options){
+		var defaults = {
+			filterChildSelector: null,
+			filter: function(el, val){
+				return $(el).text().toUpperCase().indexOf(val.toUpperCase()) >= 0;
+			},
+			before: function(){},
+			after: function(){}
+		};
+		options = $.extend(defaults, options);
+
+		var el = $(this).find(filterEl);
+		if (options.filterChildSelector) {
+			el = el.find(options.filterChildSelector);
+		}
+
+		var filter = options.filter;
+		$(inputEl).keyup(function(){
+			var val = $(this).val();
+			var contains = el.filter(function(){
+				return filter(this, val);
+			});
+			var containsNot = el.not(contains);
+			if (options.filterChildSelector){
+				contains = contains.parents(filterEl);
+				containsNot = containsNot.parents(filterEl).hide();
+			}
+
+			options.before.call(this, contains, containsNot);
+
+			contains.show();
+			containsNot.hide();
+
+			if (val === '') {
+				contains.show();
+				containsNot.show();
+			}
+
+			options.after.call(this, contains, containsNot);
+		});
+	};
+})(jQuery);
Index: /trunk/src/wp-includes/class-wp-customize-control.php
===================================================================
--- /trunk/src/wp-includes/class-wp-customize-control.php	(revision 27418)
+++ /trunk/src/wp-includes/class-wp-customize-control.php	(revision 27419)
@@ -815,2 +815,74 @@
 	}
 }
+
+/**
+ * Widget Area Customize Control Class
+ *
+ */
+class WP_Widget_Area_Customize_Control extends WP_Customize_Control {
+	public $type = 'sidebar_widgets';
+	public $sidebar_id;
+
+	public function to_json() {
+		parent::to_json();
+		$exported_properties = array( 'sidebar_id' );
+		foreach ( $exported_properties as $key ) {
+			$this->json[ $key ] = $this->$key;
+		}
+	}
+
+	public function render_content() {
+		?>
+		<span class="button-secondary add-new-widget" tabindex="0">
+			<?php esc_html_e( 'Add a Widget' ); ?>
+		</span>
+
+		<span class="reorder-toggle" tabindex="0">
+			<span class="reorder"><?php esc_html_e( 'Reorder' ); ?></span>
+			<span class="reorder-done"><?php esc_html_e( 'Done' ); ?></span>
+		</span>
+		<?php
+	}
+}
+
+/**
+ * Widget Form Customize Control Class
+ */
+class WP_Widget_Form_Customize_Control extends WP_Customize_Control {
+	public $type = 'widget_form';
+	public $widget_id;
+	public $widget_id_base;
+	public $sidebar_id;
+	public $is_new = false;
+	public $width;
+	public $height;
+	public $is_wide = false;
+	public $is_live_previewable = false;
+
+	public function to_json() {
+		parent::to_json();
+		$exported_properties = array( 'widget_id', 'widget_id_base', 'sidebar_id', 'width', 'height', 'is_wide', 'is_live_previewable' );
+		foreach ( $exported_properties as $key ) {
+			$this->json[ $key ] = $this->$key;
+		}
+	}
+
+	public function render_content() {
+		global $wp_registered_widgets;
+		require_once ABSPATH . '/wp-admin/includes/widgets.php';
+
+		$widget = $wp_registered_widgets[ $this->widget_id ];
+		if ( ! isset( $widget['params'][0] ) ) {
+			$widget['params'][0] = array();
+		}
+
+		$args = array(
+			'widget_id' => $widget['id'],
+			'widget_name' => $widget['name'],
+		);
+
+		$args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
+		echo WP_Customize_Widgets::get_widget_control( $args );
+	}
+}
+
Index: /trunk/src/wp-includes/class-wp-customize-manager.php
===================================================================
--- /trunk/src/wp-includes/class-wp-customize-manager.php	(revision 27418)
+++ /trunk/src/wp-includes/class-wp-customize-manager.php	(revision 27419)
@@ -62,4 +62,7 @@
 		require( ABSPATH . WPINC . '/class-wp-customize-section.php' );
 		require( ABSPATH . WPINC . '/class-wp-customize-control.php' );
+		require( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
+
+		WP_Customize_Widgets::setup(); // This should be integrated.
 
 		add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
Index: /trunk/src/wp-includes/class-wp-customize-widgets.php
===================================================================
--- /trunk/src/wp-includes/class-wp-customize-widgets.php	(revision 27419)
+++ /trunk/src/wp-includes/class-wp-customize-widgets.php	(revision 27419)
@@ -0,0 +1,1264 @@
+<?php
+/**
+ * Widget customizer manager class.
+ */
+class WP_Customize_Widgets {
+	const UPDATE_WIDGET_AJAX_ACTION    = 'update-widget';
+	const UPDATE_WIDGET_NONCE_POST_KEY = 'update-sidebar-widgets-nonce';
+
+	/**
+	 * All id_bases for widgets defined in core
+	 *
+	 * @var array
+	 */
+	protected static $core_widget_id_bases = array(
+		'archives',
+		'calendar',
+		'categories',
+		'links',
+		'meta',
+		'nav_menu',
+		'pages',
+		'recent-comments',
+		'recent-posts',
+		'rss',
+		'search',
+		'tag_cloud',
+		'text',
+	);
+
+	/**
+	 * Initial loader.
+	 */
+	static function setup() {
+		add_action( 'after_setup_theme', array( __CLASS__, 'setup_widget_addition_previews' ) );
+		add_action( 'customize_controls_init', array( __CLASS__, 'customize_controls_init' ) );
+		add_action( 'customize_register', array( __CLASS__, 'schedule_customize_register' ), 1 );
+		add_action( 'customize_controls_enqueue_scripts', array( __CLASS__, 'customize_controls_enqueue_deps' ) );
+		add_action( 'customize_controls_print_footer_scripts', array( __CLASS__, 'output_widget_control_templates' ) );
+		add_action( 'customize_preview_init', array( __CLASS__, 'customize_preview_init' ) );
+
+		add_action( 'dynamic_sidebar', array( __CLASS__, 'tally_rendered_widgets' ) );
+		add_action( 'dynamic_sidebar', array( __CLASS__, 'tally_sidebars_via_dynamic_sidebar_actions' ) );
+		add_filter( 'temp_is_active_sidebar', array( __CLASS__, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
+		add_filter( 'temp_dynamic_sidebar_has_widgets', array( __CLASS__, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
+
+		/**
+		 * Special filter for Settings Revisions plugin until it can handle
+		 * dynamically creating settings. Normally this should be handled by
+		 * a setting's sanitize_js_callback, but when restoring an old revision
+		 * it may include settings which do not currently exist, and so they
+		 * do not have the opportunity to be sanitized as needed. Furthermore,
+		 * we have to add this filter here because the customizer is not
+		 * initialized in WP Ajax, which is where Settings Revisions currently
+		 * needs to apply this filter at times.
+		 */
+		add_filter( 'temp_customize_sanitize_js', array( __CLASS__, 'temp_customize_sanitize_js' ), 10, 2 );
+	}
+
+	/**
+	 * Get an unslashed post value, or return a default
+	 *
+	 * @param string $name
+	 * @param mixed $default
+	 * @return mixed
+	 */
+	static function get_post_value( $name, $default = null ) {
+		if ( ! isset( $_POST[$name] ) ) {
+			return $default;
+		}
+		return wp_unslash( $_POST[$name] );
+	}
+
+	protected static $_customized;
+	protected static $_prepreview_added_filters = array();
+
+	/**
+	 * Since the widgets get registered (widgets_init) before the customizer settings are set up (customize_register),
+	 * we have to filter the options similarly to how the setting previewer will filter the options later.
+	 *
+	 * @action after_setup_theme
+	 */
+	static function setup_widget_addition_previews() {
+		global $wp_customize;
+		$is_customize_preview = (
+			( ! empty( $wp_customize ) )
+			&&
+			( ! is_admin() )
+			&&
+			( 'on' === self::get_post_value( 'wp_customize' ) )
+			&&
+			check_ajax_referer( 'preview-customize_' . $wp_customize->get_stylesheet(), 'nonce', false )
+		);
+
+		$is_ajax_widget_update = (
+			( defined( 'DOING_AJAX' ) && DOING_AJAX )
+			&&
+			self::get_post_value( 'action' ) === self::UPDATE_WIDGET_AJAX_ACTION
+			&&
+			check_ajax_referer( self::UPDATE_WIDGET_AJAX_ACTION, self::UPDATE_WIDGET_NONCE_POST_KEY, false )
+		);
+
+		$is_ajax_customize_save = (
+			( defined( 'DOING_AJAX' ) && DOING_AJAX )
+			&&
+			self::get_post_value( 'action' ) === 'customize_save'
+			&&
+			check_ajax_referer( 'save-customize_' . $wp_customize->get_stylesheet(), 'nonce' )
+		);
+
+		$is_valid_request = ( $is_ajax_widget_update || $is_customize_preview || $is_ajax_customize_save );
+		if ( ! $is_valid_request ) {
+			return;
+		}
+
+		// Input from customizer preview
+		if ( isset( $_POST['customized'] ) ) {
+			$customized = json_decode( self::get_post_value( 'customized' ), true );
+		}
+		// Input from ajax widget update request
+		else {
+			$customized    = array();
+			$id_base       = self::get_post_value( 'id_base' );
+			$widget_number = (int) self::get_post_value( 'widget_number' );
+			$option_name   = 'widget_' . $id_base;
+			$customized[$option_name] = array();
+			if ( false !== $widget_number ) {
+				$option_name .= '[' . $widget_number . ']';
+				$customized[$option_name][$widget_number] = array();
+			}
+		}
+
+		$function = array( __CLASS__, 'prepreview_added_sidebars_widgets' );
+
+		$hook = 'option_sidebars_widgets';
+		add_filter( $hook, $function );
+		self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
+
+		$hook = 'default_option_sidebars_widgets';
+		add_filter( $hook, $function );
+		self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
+
+		foreach ( $customized as $setting_id => $value ) {
+			if ( preg_match( '/^(widget_.+?)(\[(\d+)\])?$/', $setting_id, $matches ) ) {
+				$body     = sprintf( 'return %s::prepreview_added_widget_instance( $value, %s );', __CLASS__, var_export( $setting_id, true ) );
+				$function = create_function( '$value', $body );
+				$option   = $matches[1];
+
+				$hook = sprintf( 'option_%s', $option );
+				add_filter( $hook, $function );
+				self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
+
+				$hook = sprintf( 'default_option_%s', $option );
+				add_filter( $hook, $function );
+				self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
+
+				/**
+				 * Make sure the option is registered so that the update_option won't fail due to
+				 * the filters providing a default value, which causes the update_option() to get confused.
+				 */
+				add_option( $option, array() );
+			}
+		}
+
+		self::$_customized = $customized;
+	}
+
+	/**
+	 * Ensure that newly-added widgets will appear in the widgets_sidebars.
+	 * This is necessary because the customizer's setting preview filters are added after the widgets_init action,
+	 * which is too late for the widgets to be set up properly.
+	 *
+	 * @param array $sidebars_widgets
+	 * @return array
+	 */
+	static function prepreview_added_sidebars_widgets( $sidebars_widgets ) {
+		foreach ( self::$_customized as $setting_id => $value ) {
+			if ( preg_match( '/^sidebars_widgets\[(.+?)\]$/', $setting_id, $matches ) ) {
+				$sidebar_id = $matches[1];
+				$sidebars_widgets[$sidebar_id] = $value;
+			}
+		}
+		return $sidebars_widgets;
+	}
+
+	/**
+	 * Ensure that newly-added widgets will have empty instances so that they will be recognized.
+	 * This is necessary because the customizer's setting preview filters are added after the widgets_init action,
+	 * which is too late for the widgets to be set up properly.
+	 *
+	 * @param array $instance
+	 * @param string $setting_id
+	 * @return array
+	 */
+	static function prepreview_added_widget_instance( $instance, $setting_id ) {
+		if ( isset( self::$_customized[$setting_id] ) ) {
+			$parsed_setting_id = self::parse_widget_setting_id( $setting_id );
+			$widget_number     = $parsed_setting_id['number'];
+
+			// Single widget
+			if ( is_null( $widget_number ) ) {
+				if ( false === $instance && empty( $value ) ) {
+					$instance = array();
+				}
+			}
+			// Multi widget
+			else if ( false === $instance || ! isset( $instance[$widget_number] ) ) {
+				if ( empty( $instance ) ) {
+					$instance = array( '_multiwidget' => 1 );
+				}
+				if ( ! isset( $instance[$widget_number] ) ) {
+					$instance[$widget_number] = array();
+				}
+			}
+		}
+		return $instance;
+	}
+
+	/**
+	 * Remove filters added in setup_widget_addition_previews() which ensure that
+	 * widgets are populating the options during widgets_init
+	 *
+	 * @action wp_loaded
+	 */
+	static function remove_prepreview_filters() {
+		foreach ( self::$_prepreview_added_filters as $prepreview_added_filter ) {
+			remove_filter( $prepreview_added_filter['hook'], $prepreview_added_filter['function'] );
+		}
+		self::$_prepreview_added_filters = array();
+	}
+
+	/**
+	 * Make sure that all widgets get loaded into customizer; these actions are also done in the wp_ajax_save_widget()
+	 *
+	 * @see wp_ajax_save_widget()
+	 * @action customize_controls_init
+	 */
+	static function customize_controls_init() {
+		do_action( 'load-widgets.php' );
+		do_action( 'widgets.php' );
+		do_action( 'sidebar_admin_setup' );
+	}
+
+	/**
+	 * When in preview, invoke customize_register for settings after WordPress is
+	 * loaded so that all filters have been initialized (e.g. Widget Visibility)
+	 */
+	static function schedule_customize_register( $wp_customize ) {
+		if ( is_admin() ) { // @todo for some reason, $wp_customize->is_preview() is true here?
+			self::customize_register( $wp_customize );
+		} else {
+			add_action( 'wp', array( __CLASS__, 'customize_register' ) );
+		}
+	}
+
+	/**
+	 * Register customizer settings and controls for all sidebars and widgets
+	 *
+	 * @action customize_register
+	 */
+	static function customize_register( $wp_customize = null ) {
+		global $wp_registered_widgets, $wp_registered_widget_controls;
+		if ( ! ( $wp_customize instanceof WP_Customize_Manager ) ) {
+			$wp_customize = $GLOBALS['wp_customize'];
+		}
+
+		$sidebars_widgets = array_merge(
+			array( 'wp_inactive_widgets' => array() ),
+			array_fill_keys( array_keys( $GLOBALS['wp_registered_sidebars'] ), array() ),
+			wp_get_sidebars_widgets()
+		);
+
+		$new_setting_ids = array();
+
+		/**
+		 * Register a setting for all widgets, including those which are active, inactive, and orphaned
+		 * since a widget may get suppressed from a sidebar via a plugin (like Widget Visibility).
+		 */
+		foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) {
+			$setting_id   = self::get_setting_id( $widget_id );
+			$setting_args = self::get_setting_args( $setting_id );
+			$setting_args['sanitize_callback']    = array( __CLASS__, 'sanitize_widget_instance' );
+			$setting_args['sanitize_js_callback'] = array( __CLASS__, 'sanitize_widget_js_instance' );
+			$wp_customize->add_setting( $setting_id, $setting_args );
+			$new_setting_ids[] = $setting_id;
+		}
+
+		foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
+			if ( empty( $sidebar_widget_ids ) ) {
+				$sidebar_widget_ids = array();
+			}
+			$is_registered_sidebar = isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] );
+			$is_inactive_widgets   = ( 'wp_inactive_widgets' === $sidebar_id );
+			$is_active_sidebar     = ( $is_registered_sidebar && ! $is_inactive_widgets );
+
+			/**
+			 * Add setting for managing the sidebar's widgets
+			 */
+			if ( $is_registered_sidebar || $is_inactive_widgets ) {
+				$setting_id   = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
+				$setting_args = self::get_setting_args( $setting_id );
+				$setting_args['sanitize_callback']    = array( __CLASS__, 'sanitize_sidebar_widgets' );
+				$setting_args['sanitize_js_callback'] = array( __CLASS__, 'sanitize_sidebar_widgets_js_instance' );
+				$wp_customize->add_setting( $setting_id, $setting_args );
+				$new_setting_ids[] = $setting_id;
+
+				/**
+				 * Add section to contain controls
+				 */
+				$section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
+				if ( $is_active_sidebar ) {
+					$section_args = array(
+						'title' => sprintf( __( 'Widgets: %s' ), $GLOBALS['wp_registered_sidebars'][$sidebar_id]['name'] ),
+						'description' => $GLOBALS['wp_registered_sidebars'][$sidebar_id]['description'],
+					);
+					$section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
+					$wp_customize->add_section( $section_id, $section_args );
+
+					$control = new WP_Widget_Area_Customize_Control(
+						$wp_customize,
+						$setting_id,
+						array(
+							'section' => $section_id,
+							'sidebar_id' => $sidebar_id,
+							//'priority' => 99, // so it appears at the end
+						)
+					);
+					$new_setting_ids[] = $setting_id;
+					$wp_customize->add_control( $control );
+				}
+			}
+
+			/**
+			 * Add a control for each active widget (located in a sidebar)
+			 */
+			foreach ( $sidebar_widget_ids as $i => $widget_id ) {
+				// Skip widgets that may have gone away due to a plugin being deactivated
+				if ( ! $is_active_sidebar || ! isset( $GLOBALS['wp_registered_widgets'][$widget_id] ) ) {
+					continue;
+				}
+				$registered_widget = $GLOBALS['wp_registered_widgets'][$widget_id];
+				$setting_id = self::get_setting_id( $widget_id );
+				$id_base = $GLOBALS['wp_registered_widget_controls'][$widget_id]['id_base'];
+				assert( false !== is_active_widget( $registered_widget['callback'], $registered_widget['id'], false, false ) );
+				$control = new WP_Widget_Form_Customize_Control(
+					$wp_customize,
+					$setting_id,
+					array(
+						'label' => $registered_widget['name'],
+						'section' => $section_id,
+						'sidebar_id' => $sidebar_id,
+						'widget_id' => $widget_id,
+						'widget_id_base' => $id_base,
+						'priority' => $i,
+						'width' => $wp_registered_widget_controls[$widget_id]['width'],
+						'height' => $wp_registered_widget_controls[$widget_id]['height'],
+						'is_wide' => self::is_wide_widget( $widget_id ),
+					)
+				);
+				$wp_customize->add_control( $control );
+			}
+		}
+
+		/**
+		 * We have to register these settings later than customize_preview_init so that other
+		 * filters have had a chance to run.
+		 * @see self::schedule_customize_register()
+		 */
+		if ( did_action( 'customize_preview_init' ) ) {
+			foreach ( $new_setting_ids as $new_setting_id ) {
+				$wp_customize->get_setting( $new_setting_id )->preview();
+			}
+		}
+
+		self::remove_prepreview_filters();
+	}
+
+	/**
+	 * Covert a widget_id into its corresponding customizer setting id (option name)
+	 *
+	 * @param string $widget_id
+	 * @see _get_widget_id_base()
+	 * @return string
+	 */
+	static function get_setting_id( $widget_id ) {
+		$parsed_widget_id = self::parse_widget_id( $widget_id );
+		$setting_id = sprintf( 'widget_%s', $parsed_widget_id['id_base'] );
+		if ( ! is_null( $parsed_widget_id['number'] ) ) {
+			$setting_id .= sprintf( '[%d]', $parsed_widget_id['number'] );
+		}
+		return $setting_id;
+	}
+
+	/**
+	 * Core widgets which may have controls wider than 250, but can still be
+	 * shown in the narrow customizer panel. The RSS and Text widgets in Core,
+	 * for example, have widths of 400 and yet they still render fine in the
+	 * customizer panel. This method will return all Core widgets as being
+	 * not wide, but this can be overridden with the is_wide_widget_in_customizer
+	 * filter.
+	 *
+	 * @param string $widget_id
+	 * @return bool
+	 */
+	static function is_wide_widget( $widget_id ) {
+		global $wp_registered_widget_controls;
+		$parsed_widget_id = self::parse_widget_id( $widget_id );
+		$width = $wp_registered_widget_controls[$widget_id]['width'];
+		$is_core = in_array( $parsed_widget_id['id_base'], self::$core_widget_id_bases );
+		$is_wide = ( $width > 250 && ! $is_core );
+		$is_wide = apply_filters( 'is_wide_widget_in_customizer', $is_wide, $widget_id );
+		return $is_wide;
+	}
+
+	/**
+	 * Covert a widget ID into its id_base and number components
+	 *
+	 * @param string $widget_id
+	 * @return array
+	 */
+	static function parse_widget_id( $widget_id ) {
+		$parsed = array(
+			'number' => null,
+			'id_base' => null,
+		);
+		if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
+			$parsed['id_base'] = $matches[1];
+			$parsed['number']  = intval( $matches[2] );
+		} else {
+			// likely an old single widget
+			$parsed['id_base'] = $widget_id;
+		}
+		return $parsed;
+	}
+
+	/**
+	 * Convert a widget setting ID (option path) to its id_base and number components
+	 *
+	 * @throws Widget_Customizer_Exception
+	 * @throws Exception
+	 *
+	 * @param string $setting_id
+	 * @param array
+	 * @return array
+	 */
+	static function parse_widget_setting_id( $setting_id ) {
+		if ( ! preg_match( '/^(widget_(.+?))(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
+			throw new Widget_Customizer_Exception( sprintf( 'Invalid widget setting ID: %s', $setting_id ) );
+		}
+		$id_base = $matches[2];
+		$number  = isset( $matches[3] ) ? intval( $matches[3] ) : null;
+		return compact( 'id_base', 'number' );
+	}
+
+	/**
+	 * Enqueue scripts and styles for customizer panel and export data to JS
+	 *
+	 * @action customize_controls_enqueue_scripts
+	 */
+	static function customize_controls_enqueue_deps() {
+		wp_enqueue_script( 'jquery-ui-sortable' );
+		wp_enqueue_script( 'jquery-ui-droppable' );
+		wp_enqueue_style(
+			'widget-customizer',
+			admin_url( 'css/customize-widgets.css' )
+		);
+		wp_enqueue_script(
+			'widget-customizer',
+			admin_url( 'js/customize-widgets.js' ),
+			array( 'jquery', 'wp-backbone', 'wp-util', 'customize-controls' )
+		);
+
+		// Export available widgets with control_tpl removed from model
+		// since plugins need templates to be in the DOM
+		$available_widgets = array();
+		foreach ( self::get_available_widgets() as $available_widget ) {
+			unset( $available_widget['control_tpl'] );
+			$available_widgets[] = $available_widget;
+		}
+
+		$widget_reorder_nav_tpl = sprintf(
+			'<div class="widget-reorder-nav"><span class="move-widget" tabindex="0" title="%1$s">%2$s</span><span class="move-widget-down" tabindex="0" title="%3$s">%4$s</span><span class="move-widget-up" tabindex="0" title="%5$s">%6$s</span></div>',
+			esc_attr__( 'Move to another area...' ),
+			esc_html__( 'Move to another area...' ),
+			esc_attr__( 'Move down' ),
+			esc_html__( 'Move down' ),
+			esc_attr__( 'Move up' ),
+			esc_html__( 'Move up' )
+		);
+
+		$move_widget_area_tpl = str_replace(
+			array( '{description}', '{btn}' ),
+			array(
+				esc_html__( 'Select an area to move this widget into:' ),
+				esc_html__( 'Move' ),
+			),
+			'
+				<div class="move-widget-area">
+					<p class="description">{description}</p>
+					<ul class="widget-area-select">
+						<% _.each( sidebars, function ( sidebar ){ %>
+							<li class="" data-id="<%- sidebar.id %>" title="<%- sidebar.description %>" tabindex="0"><%- sidebar.name %></li>
+						<% }); %>
+					</ul>
+					<div class="move-widget-actions">
+						<button class="move-widget-btn button-secondary" type="button">{btn}</button>
+					</div>
+				</div>
+			'
+		);
+
+		// Why not wp_localize_script? Because we're not localizing, and it forces values into strings
+		global $wp_scripts;
+		$exports = array(
+			'update_widget_ajax_action' => self::UPDATE_WIDGET_AJAX_ACTION,
+			'update_widget_nonce_value' => wp_create_nonce( self::UPDATE_WIDGET_AJAX_ACTION ),
+			'update_widget_nonce_post_key' => self::UPDATE_WIDGET_NONCE_POST_KEY,
+			'registered_sidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
+			'registered_widgets' => $GLOBALS['wp_registered_widgets'],
+			'available_widgets' => $available_widgets, // @todo Merge this with registered_widgets
+			'i18n' => array(
+				'save_btn_label' => _x( 'Apply', 'button to save changes to a widget' ),
+				'save_btn_tooltip' => _x( 'Save and preview changes before publishing them.', 'tooltip on the widget save button' ),
+				'remove_btn_label' => _x( 'Remove', 'link to move a widget to the inactive widgets sidebar' ),
+				'remove_btn_tooltip' => _x( 'Trash widget by moving it to the inactive widgets sidebar.', 'tooltip on btn a widget to move it to the inactive widgets sidebar' ),
+			),
+			'tpl' => array(
+				'widget_reorder_nav' => $widget_reorder_nav_tpl,
+				'move_widget_area' => $move_widget_area_tpl,
+			),
+		);
+		foreach ( $exports['registered_widgets'] as &$registered_widget ) {
+			unset( $registered_widget['callback'] ); // may not be JSON-serializeable
+		}
+
+		$wp_scripts->add_data(
+			'widget-customizer',
+			'data',
+			sprintf( 'var WidgetCustomizer_exports = %s;', json_encode( $exports ) )
+		);
+	}
+
+	/**
+	 * Render the widget form control templates into the DOM so that plugin scripts can manipulate them
+	 *
+	 * @action customize_controls_print_footer_scripts
+	 */
+	static function output_widget_control_templates() {
+		?>
+		<div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
+		<div id="available-widgets">
+			<div id="available-widgets-filter">
+				<input type="search" placeholder="<?php esc_attr_e( 'Find widgets&hellip;' ) ?>">
+			</div>
+			<?php foreach ( self::get_available_widgets() as $available_widget ): ?>
+				<div id="widget-tpl-<?php echo esc_attr( $available_widget['id'] ) ?>" data-widget-id="<?php echo esc_attr( $available_widget['id'] ) ?>" class="widget-tpl <?php echo esc_attr( $available_widget['id'] ) ?>" tabindex="0">
+					<?php echo $available_widget['control_tpl']; // xss ok ?>
+				</div>
+			<?php endforeach; ?>
+		</div><!-- #available-widgets -->
+		</div><!-- #widgets-left -->
+		<?php
+	}
+
+	/**
+	 * Get common arguments to supply when constructing a customizer setting
+	 *
+	 * @param string $id
+	 * @param array  [$overrides]
+	 * @return array
+	 */
+	static function get_setting_args( $id, $overrides = array() ) {
+		$args = array(
+			'type' => 'option',
+			'capability' => 'edit_theme_options',
+			'transport' => 'refresh',
+			'default' => array(),
+		);
+		$args = array_merge( $args, $overrides );
+		$args = apply_filters( 'widget_customizer_setting_args', $args, $id );
+		return $args;
+	}
+
+	/**
+	 * Make sure that a sidebars_widgets[x] only ever consists of actual widget IDs.
+	 * Used as sanitize_callback for each sidebars_widgets setting.
+	 *
+	 * @param array $widget_ids
+	 * @return array
+	 */
+	static function sanitize_sidebar_widgets( $widget_ids ) {
+		global $wp_registered_widgets;
+		$widget_ids = array_map( 'strval', (array) $widget_ids );
+		$sanitized_widget_ids = array();
+		foreach ( $widget_ids as $widget_id ) {
+			if ( array_key_exists( $widget_id, $wp_registered_widgets ) ) {
+				$sanitized_widget_ids[] = $widget_id;
+			}
+		}
+		return $sanitized_widget_ids;
+	}
+
+	/**
+	 * Special filter for Settings Revisions plugin until it can handle
+	 * dynamically creating settings.
+	 *
+	 * @param mixed $value
+	 * @param stdClass|WP_Customize_Setting $setting
+	 * @return mixed
+	 */
+	static function temp_customize_sanitize_js( $value, $setting ) {
+		if ( preg_match( '/^widget_/', $setting->id ) && $setting->type === 'option' ) {
+			$value = self::sanitize_widget_js_instance( $value );
+		}
+		return $value;
+	}
+
+	/**
+	 * Build up an index of all available widgets for use in Backbone models
+	 *
+	 * @see wp_list_widgets()
+	 * @return array
+	 */
+	static function get_available_widgets() {
+		static $available_widgets = array();
+		if ( ! empty( $available_widgets ) ) {
+			return $available_widgets;
+		}
+
+		global $wp_registered_widgets, $wp_registered_widget_controls;
+		require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
+
+		$sort = $wp_registered_widgets;
+		usort( $sort, array( __CLASS__, '_sort_name_callback' ) );
+		$done = array();
+
+		foreach ( $sort as $widget ) {
+			if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget
+				continue;
+			}
+
+			$sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
+			$done[]  = $widget['callback'];
+
+			if ( ! isset( $widget['params'][0] ) ) {
+				$widget['params'][0] = array();
+			}
+
+			$available_widget = $widget;
+			unset( $available_widget['callback'] ); // not serializable to JSON
+
+			$args = array(
+				'widget_id' => $widget['id'],
+				'widget_name' => $widget['name'],
+				'_display' => 'template',
+			);
+
+			$is_disabled     = false;
+			$is_multi_widget = (
+				isset( $wp_registered_widget_controls[$widget['id']]['id_base'] )
+				&&
+				isset( $widget['params'][0]['number'] )
+			);
+			if ( $is_multi_widget ) {
+				$id_base = $wp_registered_widget_controls[$widget['id']]['id_base'];
+				$args['_temp_id']   = "$id_base-__i__";
+				$args['_multi_num'] = next_widget_id_number( $id_base );
+				$args['_add']       = 'multi';
+			} else {
+				$args['_add'] = 'single';
+				if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) {
+					$is_disabled = true;
+				}
+				$id_base = $widget['id'];
+			}
+
+			$list_widget_controls_args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
+			$control_tpl = self::get_widget_control( $list_widget_controls_args );
+
+			// The properties here are mapped to the Backbone Widget model
+			$available_widget = array_merge(
+				$available_widget,
+				array(
+					'temp_id' => isset( $args['_temp_id'] ) ? $args['_temp_id'] : null,
+					'is_multi' => $is_multi_widget,
+					'control_tpl' => $control_tpl,
+					'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
+					'is_disabled' => $is_disabled,
+					'id_base' => $id_base,
+					'transport' => 'refresh',
+					'width' => $wp_registered_widget_controls[$widget['id']]['width'],
+					'height' => $wp_registered_widget_controls[$widget['id']]['height'],
+					'is_wide' => self::is_wide_widget( $widget['id'] ),
+				)
+			);
+
+			$available_widgets[] = $available_widget;
+		}
+		return $available_widgets;
+	}
+
+	/**
+	 * Replace with inline closure once on PHP 5.3:
+	 * sort( $array, function ( $a, $b ) { return strnatcasecmp( $a['name'], $b['name'] ); } );
+	 *
+	 * @access private
+	 */
+	static function _sort_name_callback( $a, $b ) {
+		return strnatcasecmp( $a['name'], $b['name'] );
+	}
+
+	/**
+	 * Invoke wp_widget_control() but capture the output buffer and transform the markup
+	 * so that it can be used in the customizer.
+	 *
+	 * @see wp_widget_control()
+	 * @param array $args
+	 * @return string
+	 */
+	static function get_widget_control( $args ) {
+		ob_start();
+		call_user_func_array( 'wp_widget_control', $args );
+		$replacements = array(
+			'<form action="" method="post">' => '<div class="form">',
+			'</form>' => '</div><!-- .form -->',
+		);
+		$control_tpl = ob_get_clean();
+		$control_tpl = str_replace( array_keys( $replacements ), array_values( $replacements ), $control_tpl );
+		return $control_tpl;
+	}
+
+	/**
+	 * Add hooks for the customizer preview
+	 *
+	 * @action customize_preview_init
+	 */
+	static function customize_preview_init() {
+		add_filter( 'sidebars_widgets', array( __CLASS__, 'preview_sidebars_widgets' ), 1 );
+		add_action( 'wp_enqueue_scripts', array( __CLASS__, 'customize_preview_enqueue_deps' ) );
+		add_action( 'wp_footer', array( __CLASS__, 'export_preview_data' ), 9999 );
+	}
+
+	/**
+	 * When previewing, make sure the proper previewing widgets are used. Because wp_get_sidebars_widgets()
+	 * gets called early at init (via wp_convert_widget_settings()) and can set global variable
+	 * $_wp_sidebars_widgets to the value of get_option( 'sidebars_widgets' ) before the customizer
+	 * preview filter is added, we have to reset it after the filter has been added.
+	 *
+	 * @filter sidebars_widgets
+	 */
+	static function preview_sidebars_widgets( $sidebars_widgets ) {
+		$sidebars_widgets = get_option( 'sidebars_widgets' );
+		unset( $sidebars_widgets['array_version'] );
+		return $sidebars_widgets;
+	}
+
+	/**
+	 * Enqueue scripts for the customizer preview
+	 *
+	 * @action wp_enqueue_scripts
+	 */
+	static function customize_preview_enqueue_deps() {
+		wp_enqueue_script(
+			'customize-preview-widgets',
+			includes_url( 'js/customize-preview-widgets.js' ),
+			array( 'jquery', 'wp-util', 'customize-preview' )
+		);
+
+		/*
+		wp_enqueue_style(
+			'widget-customizer-preview',
+			'widget-customizer-preview.css'
+		);
+		*/
+
+		// Why not wp_localize_script? Because we're not localizing, and it forces values into strings
+		global $wp_scripts;
+		$exports = array(
+			'registered_sidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
+			'registered_widgets' => $GLOBALS['wp_registered_widgets'],
+			'i18n' => array(
+				'widget_tooltip' => __( 'Press shift and then click to edit widget in customizer...' ),
+			),
+			'request_uri' => wp_unslash( $_SERVER['REQUEST_URI'] ),
+		);
+		foreach ( $exports['registered_widgets'] as &$registered_widget ) {
+			unset( $registered_widget['callback'] ); // may not be JSON-serializeable
+		}
+		$wp_scripts->add_data(
+			'customize-preview-widgets',
+			'data',
+			sprintf( 'var WidgetCustomizerPreview_exports = %s;', json_encode( $exports ) )
+		);
+	}
+
+	/**
+	 * At the very end of the page, at the very end of the wp_footer, communicate the sidebars that appeared on the page
+	 *
+	 * @action wp_footer
+	 */
+	static function export_preview_data() {
+		wp_print_scripts( array( 'customize-preview-widgets' ) );
+		?>
+		<script>
+		(function () {
+			/*global WidgetCustomizerPreview */
+			WidgetCustomizerPreview.rendered_sidebars = <?php echo json_encode( array_fill_keys( array_unique( self::$rendered_sidebars ), true ) ) ?>;
+			WidgetCustomizerPreview.rendered_widgets = <?php echo json_encode( array_fill_keys( array_keys( self::$rendered_widgets ), true ) ); ?>;
+		}());
+		</script>
+		<?php
+	}
+
+	static protected $rendered_sidebars = array();
+	static protected $rendered_widgets  = array();
+
+	/**
+	 * Keep track of the widgets that were rendered
+	 *
+	 * @action dynamic_sidebar
+	 */
+	static function tally_rendered_widgets( $widget ) {
+		self::$rendered_widgets[$widget['id']] = true;
+	}
+
+	/**
+	 * This is hacky. It is too bad that dynamic_sidebar is not just called once with the $sidebar_id supplied
+	 * This does not get called for a sidebar which lacks widgets.
+	 * See core patch which addresses the problem.
+	 *
+	 * @link http://core.trac.wordpress.org/ticket/25368
+	 * @action dynamic_sidebar
+	 */
+	static function tally_sidebars_via_dynamic_sidebar_actions( $widget ) {
+		global $sidebars_widgets;
+		foreach ( $sidebars_widgets as $sidebar_id => $widget_ids ) {
+			if ( in_array( $sidebar_id, self::$rendered_sidebars ) ) {
+				continue;
+			}
+			if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) && is_array( $widget_ids ) && in_array( $widget['id'], $widget_ids ) ) {
+				self::$rendered_sidebars[] = $sidebar_id;
+			}
+		}
+	}
+
+	/**
+	 * Keep track of the times that is_active_sidebar() is called in the template, and assume that this
+	 * means that the sidebar would be rendered on the template if there were widgets populating it.
+	 *
+	 * @see http://core.trac.wordpress.org/ticket/25368
+	 * @filter temp_is_active_sidebar
+	 */
+	static function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
+		if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
+			self::$rendered_sidebars[] = $sidebar_id;
+		}
+		// We may need to force this to true, and also force-true the value for temp_dynamic_sidebar_has_widgets
+		// if we want to ensure that there is an area to drop widgets into, if the sidebar is empty.
+		return $is_active;
+	}
+
+	/**
+	 * Keep track of the times that dynamic_sidebar() is called in the template, and assume that this
+	 * means that the sidebar would be rendered on the template if there were widgets populating it.
+	 *
+	 * @see http://core.trac.wordpress.org/ticket/25368
+	 * @filter temp_dynamic_sidebar_has_widgets
+	 */
+	static function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
+		if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
+			self::$rendered_sidebars[] = $sidebar_id;
+		}
+		// We may need to force this to true, and also force-true the value for temp_is_active_sidebar
+		// if we want to ensure that there is an area to drop widgets into, if the sidebar is empty.
+		return $has_widgets;
+	}
+
+	/**
+	 * Serialize an instance and hash it with the AUTH_KEY; when a JS value is
+	 * posted back to save, this instance hash key is used to ensure that the
+	 * serialized_instance was not tampered with, but that it had originated
+	 * from WordPress and so is sanitized.
+	 *
+	 * @param array $instance
+	 * @return string
+	 */
+	protected static function get_instance_hash_key( $instance ) {
+		$hash = md5( AUTH_KEY . serialize( $instance ) );
+		return $hash;
+	}
+
+	/**
+	 * Unserialize the JS-instance for storing in the options. It's important
+	 * that this filter only get applied to an instance once.
+	 *
+	 * @see Widget_Customizer::sanitize_widget_js_instance()
+	 *
+	 * @param array $value
+	 * @return array
+	 */
+	static function sanitize_widget_instance( $value ) {
+		if ( $value === array() ) {
+			return $value;
+		}
+		$invalid = (
+			empty( $value['is_widget_customizer_js_value'] )
+			||
+			empty( $value['instance_hash_key'] )
+			||
+			empty( $value['encoded_serialized_instance'] )
+		);
+		if ( $invalid ) {
+			return null;
+		}
+		$decoded = base64_decode( $value['encoded_serialized_instance'], true );
+		if ( false === $decoded ) {
+			return null;
+		}
+		$instance = unserialize( $decoded );
+		if ( false === $instance ) {
+			return null;
+		}
+		if ( self::get_instance_hash_key( $instance ) !== $value['instance_hash_key'] ) {
+			return null;
+		}
+		return $instance;
+	}
+
+	/**
+	 * Convert widget instance into JSON-representable format
+	 *
+	 * @see Widget_Customizer::sanitize_widget_instance()
+	 *
+	 * @param array $value
+	 * @return array
+	 */
+	static function sanitize_widget_js_instance( $value ) {
+		if ( empty( $value['is_widget_customizer_js_value'] ) ) {
+			$serialized = serialize( $value );
+			$value = array(
+				'encoded_serialized_instance' => base64_encode( $serialized ),
+				'title' => empty( $value['title'] ) ? '' : $value['title'],
+				'is_widget_customizer_js_value' => true,
+				'instance_hash_key' => self::get_instance_hash_key( $value ),
+			);
+		}
+		return $value;
+	}
+
+	/**
+	 * Strip out widget IDs for widgets which are no longer registered, such
+	 * as the case when a plugin orphans a widget in a sidebar when it is deactivated.
+	 *
+	 * @param array $widget_ids
+	 * @return array
+	 */
+	static function sanitize_sidebar_widgets_js_instance( $widget_ids ) {
+		global $wp_registered_widgets;
+		$widget_ids = array_values( array_intersect( $widget_ids, array_keys( $wp_registered_widgets ) ) );
+		return $widget_ids;
+	}
+
+	/**
+	 * Find and invoke the widget update and control callbacks. Requires that
+	 * $_POST be populated with the instance data.
+	 *
+	 * @throws Widget_Customizer_Exception
+	 * @throws Exception
+	 *
+	 * @param string $widget_id
+	 * @return array
+	 */
+	static function call_widget_update( $widget_id ) {
+		global $wp_registered_widget_updates, $wp_registered_widget_controls;
+
+		$options_transaction = new Options_Transaction();
+
+		try {
+			$options_transaction->start();
+			$parsed_id   = self::parse_widget_id( $widget_id );
+			$option_name = 'widget_' . $parsed_id['id_base'];
+
+			/**
+			 * If a previously-sanitized instance is provided, populate the input vars
+			 * with its values so that the widget update callback will read this instance
+			 */
+			$added_input_vars = array();
+			if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
+				$sanitized_widget_setting = json_decode( self::get_post_value( 'sanitized_widget_setting' ), true );
+				if ( empty( $sanitized_widget_setting ) ) {
+					throw new Widget_Customizer_Exception( 'Malformed sanitized_widget_setting' );
+				}
+				$instance = self::sanitize_widget_instance( $sanitized_widget_setting );
+				if ( is_null( $instance ) ) {
+					throw new Widget_Customizer_Exception( 'Unsanitary sanitized_widget_setting' );
+				}
+				if ( ! is_null( $parsed_id['number'] ) ) {
+					$value = array();
+					$value[$parsed_id['number']] = $instance;
+					$key = 'widget-' . $parsed_id['id_base'];
+					$_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
+					$added_input_vars[] = $key;
+				} else {
+					foreach ( $instance as $key => $value ) {
+						$_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
+						$added_input_vars[] = $key;
+					}
+				}
+			}
+
+			/**
+			 * Invoke the widget update callback
+			 */
+			foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
+				if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
+					ob_start();
+					call_user_func_array( $control['callback'], $control['params'] );
+					ob_end_clean();
+					break;
+				}
+			}
+
+			// Clean up any input vars that were manually added
+			foreach ( $added_input_vars as $key ) {
+				unset( $_POST[$key] );
+				unset( $_REQUEST[$key] );
+			}
+
+			/**
+			 * Make sure the expected option was updated
+			 */
+			if ( 0 !== $options_transaction->count() ) {
+				if ( count( $options_transaction->options ) > 1 ) {
+					throw new Widget_Customizer_Exception( sprintf( 'Widget %1$s unexpectedly updated more than one option.', $widget_id ) );
+				}
+				$updated_option_name = key( $options_transaction->options );
+				if ( $updated_option_name !== $option_name ) {
+					throw new Widget_Customizer_Exception( sprintf( 'Widget %1$s updated option "%2$s", but expected "%3$s".', $widget_id, $updated_option_name, $option_name ) );
+				}
+			}
+
+			/**
+			 * Obtain the widget control with the updated instance in place
+			 */
+			ob_start();
+			$form = $wp_registered_widget_controls[$widget_id];
+			if ( $form ) {
+				call_user_func_array( $form['callback'], $form['params'] );
+			}
+			$form = ob_get_clean();
+
+			/**
+			 * Obtain the widget instance
+			 */
+			$option = get_option( $option_name );
+			if ( null !== $parsed_id['number'] ) {
+				$instance = $option[$parsed_id['number']];
+			} else {
+				$instance = $option;
+			}
+
+			$options_transaction->rollback();
+			return compact( 'instance', 'form' );
+		}
+		catch ( Exception $e ) {
+			$options_transaction->rollback();
+			throw $e;
+		}
+	}
+
+	/**
+	 * Allow customizer to update a widget using its form, but return the new
+	 * instance info via Ajax instead of saving it to the options table.
+	 * Most code here copied from wp_ajax_save_widget()
+	 *
+	 * @see wp_ajax_save_widget
+	 * @todo Reuse wp_ajax_save_widget now that we have option transactions?
+	 * @action wp_ajax_update_widget
+	 */
+	static function wp_ajax_update_widget() {
+		$generic_error = __( 'An error has occurred. Please reload the page and try again.' );
+
+		try {
+			if ( ! check_ajax_referer( self::UPDATE_WIDGET_AJAX_ACTION, self::UPDATE_WIDGET_NONCE_POST_KEY, false ) ) {
+				throw new Widget_Customizer_Exception( __( 'Nonce check failed. Reload and try again?' ) );
+			}
+			if ( ! current_user_can( 'edit_theme_options' ) ) {
+				throw new Widget_Customizer_Exception( __( 'Current user cannot!' ) );
+			}
+			if ( ! isset( $_POST['widget-id'] ) ) {
+				throw new Widget_Customizer_Exception( __( 'Incomplete request' ) );
+			}
+
+			unset( $_POST[self::UPDATE_WIDGET_NONCE_POST_KEY], $_POST['action'] );
+
+			do_action( 'load-widgets.php' );
+			do_action( 'widgets.php' );
+			do_action( 'sidebar_admin_setup' );
+
+			$widget_id = self::get_post_value( 'widget-id' );
+			$parsed_id = self::parse_widget_id( $widget_id );
+			$id_base   = $parsed_id['id_base'];
+
+			if ( isset( $_POST['widget-' . $id_base] ) && is_array( $_POST['widget-' . $id_base] ) && preg_match( '/__i__|%i%/', key( $_POST['widget-' . $id_base] ) ) ) {
+				throw new Widget_Customizer_Exception( 'Cannot pass widget templates to create new instances; apply template vars in JS' );
+			}
+
+			$updated_widget = self::call_widget_update( $widget_id ); // => {instance,form}
+			$form = $updated_widget['form'];
+			$instance = self::sanitize_widget_js_instance( $updated_widget['instance'] );
+
+			wp_send_json_success( compact( 'form', 'instance' ) );
+		}
+		catch( Exception $e ) {
+			if ( $e instanceof Widget_Customizer_Exception ) {
+				$message = $e->getMessage();
+			} else {
+				error_log( sprintf( '%s in %s: %s', get_class( $e ), __FUNCTION__, $e->getMessage() ) );
+				$message = $generic_error;
+			}
+			wp_send_json_error( compact( 'message' ) );
+		}
+	}
+}
+
+class Widget_Customizer_Exception extends Exception {}
+
+class Options_Transaction {
+
+	/**
+	 * @var array $options values updated while transaction is open
+	 */
+	public $options = array();
+
+	protected $_ignore_transients = true;
+	protected $_is_current = false;
+	protected $_operations = array();
+
+	function __construct( $ignore_transients = true ) {
+		$this->_ignore_transients = $ignore_transients;
+	}
+
+	/**
+	 * Determine whether or not the transaction is open
+	 * @return bool
+	 */
+	function is_current() {
+		return $this->_is_current;
+	}
+
+	/**
+	 * @param $option_name
+	 * @return boolean
+	 */
+	function is_option_ignored( $option_name ) {
+		return ( $this->_ignore_transients && 0 === strpos( $option_name, '_transient_' ) );
+	}
+
+	/**
+	 * Get the number of operations performed in the transaction
+	 * @return bool
+	 */
+	function count() {
+		return count( $this->_operations );
+	}
+
+	/**
+	 * Start keeping track of changes to options, and cache their new values
+	 */
+	function start() {
+		$this->_is_current = true;
+		add_action( 'added_option', array( $this, '_capture_added_option' ), 10, 2 );
+		add_action( 'updated_option', array( $this, '_capture_updated_option' ), 10, 3 );
+		add_action( 'delete_option', array( $this, '_capture_pre_deleted_option' ), 10, 1 );
+		add_action( 'deleted_option', array( $this, '_capture_deleted_option' ), 10, 1 );
+	}
+
+	/**
+	 * @action added_option
+	 * @param $option_name
+	 * @param $new_value
+	 */
+	function _capture_added_option( $option_name, $new_value ) {
+		if ( $this->is_option_ignored( $option_name ) ) {
+			return;
+		}
+		$this->options[$option_name] = $new_value;
+		$operation = 'add';
+		$this->_operations[] = compact( 'operation', 'option_name', 'new_value' );
+	}
+
+	/**
+	 * @action updated_option
+	 * @param string $option_name
+	 * @param mixed $old_value
+	 * @param mixed $new_value
+	 */
+	function _capture_updated_option( $option_name, $old_value, $new_value ) {
+		if ( $this->is_option_ignored( $option_name ) ) {
+			return;
+		}
+		$this->options[$option_name] = $new_value;
+		$operation = 'update';
+		$this->_operations[] = compact( 'operation', 'option_name', 'old_value', 'new_value' );
+	}
+
+	protected $_pending_delete_option_autoload;
+	protected $_pending_delete_option_value;
+
+	/**
+	 * It's too bad the old_value and autoload aren't passed into the deleted_option action
+	 * @action delete_option
+	 * @param string $option_name
+	 */
+	function _capture_pre_deleted_option( $option_name ) {
+		if ( $this->is_option_ignored( $option_name ) ) {
+			return;
+		}
+		global $wpdb;
+		$autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option_name ) ); // db call ok; no-cache ok
+		$this->_pending_delete_option_autoload = $autoload;
+		$this->_pending_delete_option_value    = get_option( $option_name );
+	}
+
+	/**
+	 * @action deleted_option
+	 * @param string $option_name
+	 */
+	function _capture_deleted_option( $option_name ) {
+		if ( $this->is_option_ignored( $option_name ) ) {
+			return;
+		}
+		unset( $this->options[$option_name] );
+		$operation = 'delete';
+		$old_value = $this->_pending_delete_option_value;
+		$autoload  = $this->_pending_delete_option_autoload;
+		$this->_operations[] = compact( 'operation', 'option_name', 'old_value', 'autoload' );
+	}
+
+	/**
+	 * Undo any changes to the options since start() was called
+	 */
+	function rollback() {
+		remove_action( 'updated_option', array( $this, '_capture_updated_option' ), 10, 3 );
+		remove_action( 'added_option', array( $this, '_capture_added_option' ), 10, 2 );
+		remove_action( 'delete_option', array( $this, '_capture_pre_deleted_option' ), 10, 1 );
+		remove_action( 'deleted_option', array( $this, '_capture_deleted_option' ), 10, 1 );
+		while ( 0 !== count( $this->_operations ) ) {
+			$option_operation = array_pop( $this->_operations );
+			if ( 'add' === $option_operation['operation'] ) {
+				delete_option( $option_operation['option_name'] );
+			}
+			else if ( 'delete' === $option_operation['operation'] ) {
+				add_option( $option_operation['option_name'], $option_operation['old_value'], null, $option_operation['autoload'] );
+			}
+			else if ( 'update' === $option_operation['operation'] ) {
+				update_option( $option_operation['option_name'], $option_operation['old_value'] );
+			}
+			else {
+				throw new Exception( 'Unexpected operation' );
+			}
+		}
+		$this->_is_current = false;
+	}
+}
Index: /trunk/src/wp-includes/js/customize-preview-widgets.js
===================================================================
--- /trunk/src/wp-includes/js/customize-preview-widgets.js	(revision 27419)
+++ /trunk/src/wp-includes/js/customize-preview-widgets.js	(revision 27419)
@@ -0,0 +1,115 @@
+/*global jQuery, WidgetCustomizerPreview_exports */
+/*exported WidgetCustomizerPreview */
+var WidgetCustomizerPreview = (function ($) {
+	'use strict';
+
+	var self = {
+		rendered_sidebars: {}, // @todo Make rendered a property of the Backbone model
+		rendered_widgets: {}, // @todo Make rendered a property of the Backbone model
+		registered_sidebars: [], // @todo Make a Backbone collection
+		registered_widgets: {}, // @todo Make array, Backbone collection
+		widget_selectors: [],
+		render_widget_ajax_action: null,
+		render_widget_nonce_value: null,
+		render_widget_nonce_post_key: null,
+		preview: null,
+		i18n: {},
+
+		init: function () {
+			this.buildWidgetSelectors();
+			this.highlightControls();
+
+			self.preview.bind( 'active', function() {
+				self.preview.send( 'rendered-sidebars', self.rendered_sidebars ); // @todo Only send array of IDs
+				self.preview.send( 'rendered-widgets', self.rendered_widgets ); // @todo Only send array of IDs
+			} );
+		},
+
+		/**
+		 * Calculate the selector for the sidebar's widgets based on the registered sidebar's info
+		 */
+		buildWidgetSelectors: function () {
+			$.each( self.registered_sidebars, function ( i, sidebar ) {
+				var widget_tpl = [
+					sidebar.before_widget.replace('%1$s', '').replace('%2$s', ''),
+					sidebar.before_title,
+					sidebar.after_title,
+					sidebar.after_widget
+				].join('');
+				var empty_widget = $(widget_tpl);
+				var widget_selector = empty_widget.prop('tagName');
+				var widget_classes = empty_widget.prop('className').replace(/^\s+|\s+$/g, '');
+				if ( widget_classes ) {
+					widget_selector += '.' + widget_classes.split(/\s+/).join('.');
+				}
+				self.widget_selectors.push(widget_selector);
+			});
+		},
+
+		/**
+		 * Obtain a widget instance if it was added to the provided sidebar
+		 * This addresses a race condition where a widget is moved between sidebars
+		 * We cannot use ID selector because jQuery will only return the first one
+		 * that matches. We have to resort to an [id] attribute selector
+		 *
+		 * @param {String} sidebar_id
+		 * @param {String} widget_id
+		 * @return {jQuery}
+		 */
+		getSidebarWidgetElement: function ( sidebar_id, widget_id ) {
+			return $( '[id=' + widget_id + ']' ).filter( function () {
+				return $( this ).data( 'widget_customizer_sidebar_id' ) === sidebar_id;
+			} );
+		},
+
+		/**
+		 *
+		 */
+		highlightControls: function() {
+
+			var selector = this.widget_selectors.join(',');
+
+			$(selector).attr( 'title', self.i18n.widget_tooltip );
+
+			$(document).on( 'mouseenter', selector, function () {
+				var control = parent.WidgetCustomizer.getWidgetFormControlForWidget( $(this).prop('id') );
+				if ( control ) {
+					control.highlightSectionAndControl();
+				}
+			});
+
+			// Open expand the widget control when shift+clicking the widget element
+			$(document).on( 'click', selector, function ( e ) {
+				if ( ! e.shiftKey ) {
+					return;
+				}
+				e.preventDefault();
+				var control = parent.WidgetCustomizer.getWidgetFormControlForWidget( $(this).prop('id') );
+				if ( control ) {
+					control.focus();
+				}
+			});
+		}
+
+	};
+
+	$.extend(self, WidgetCustomizerPreview_exports);
+
+	/**
+	 * Capture the instance of the Preview since it is private
+	 */
+	var OldPreview = wp.customize.Preview;
+	wp.customize.Preview = OldPreview.extend( {
+		initialize: function( params, options ) {
+			self.preview = this;
+			OldPreview.prototype.initialize.call( this, params, options );
+		}
+	} );
+
+	// @todo on customize ready?
+	$(function () {
+		self.init();
+	});
+
+	return self;
+}( jQuery ));
