diff --git a/build/css/components/drag-hint.css b/build/css/components/drag-hint.css
index 65400868988160b2de3775921b66aba37a0e000f..1e98353fafbe014122a0211bfe227a23249a1768 100644
--- a/build/css/components/drag-hint.css
+++ b/build/css/components/drag-hint.css
@@ -1,3 +1,15 @@
 .lp-hint {
+  z-index: 1000;
+}
+.lp-hint[data-orientation="vertical"] {
+  top: 0;
+  border-right: 5px solid blue;
+}
+.lp-hint[data-orientation="horizontal"] {
+  left: 0;
+  width: 100%;
   border-top: 5px solid blue;
 }
+.gu-mirror {
+  opacity: 1 !important;
+}
diff --git a/build/css/components/form.css b/build/css/components/form.css
index 9538670d8225415a97dba5ef462c3b68ed8b379e..879f250549fdee8a253c89b9adba32c3d1c9a892 100644
--- a/build/css/components/form.css
+++ b/build/css/components/form.css
@@ -52,7 +52,7 @@ mercury-dialog .form-item__description {
 .layout-paragraphs-component-form details .layout-paragraphs-component-form details:hover {
       border-color: var(--me-color-border-focus);
     }
-.layout-paragraphs-component-form input:not([type="checkbox"]):not([type="radio"]),
+.layout-paragraphs-component-form input:not([type="checkbox"]):not([type="radio"]):not(.media-library-item__remove),
   .layout-paragraphs-component-form textarea,
   .layout-paragraphs-component-form select {
     height: auto;
diff --git a/build/css/components/frontend-builder.css b/build/css/components/frontend-builder.css
index 1f0800c003d33412f52e3f2f93860e1d33c85701..5c6a444c3cb0ccdaef8624c7140860620e90310f 100644
--- a/build/css/components/frontend-builder.css
+++ b/build/css/components/frontend-builder.css
@@ -29,19 +29,23 @@
   * Component hover styles. Uses a class instead of hover state for slight
   * pause, to avoid jumpiness.
   */
-.js-lpb-component.focused {
+.lp-builder:not(.is-navigating) .js-lpb-component.focused {
   outline: 1px solid blue;
+  z-index: 1000;
 }
-.js-lpb-component.focused .js-lpb-region {
+.lp-builder:not(.is-navigating) .js-lpb-component.focused .js-lpb-region {
   outline: 1px dotted rgba(0, 0, 255, 0.5);
 }
-.js-lpb-component.focused > .js-lpb-ui {
+.lp-builder:not(.is-navigating) .js-lpb-component.focused > .js-lpb-ui {
   opacity: 1;
 }
 
 /**
  * Mercury Editor controls modifications.
  */
+.is-mercury-edit-mode .lp-builder {
+    z-index: 100;
+  }
 .is-mercury-edit-mode .lpb-controls {
     padding: 0 5px 0 0;
     border-radius: 4px;
@@ -72,6 +76,25 @@
     font-size: .7em;
     letter-spacing: 2px;
   }
+.is-mercury-edit-mode .lpb-tooltiptext {
+    left: var(--me-lpb-tooltip-text-left, -12px);
+    overflow: hidden;
+    clip: rect(1px, 1px, 1px, 1px);
+    width: 1px;
+    height: 1px;
+    word-wrap: normal;
+  }
+.is-mercury-edit-mode .lp-builder:not(.is-dragging) .lpb-tooltip--focus:focus + .lpb-tooltiptext,
+  .is-mercury-edit-mode .lp-builder:not(.is-dragging) .lpb-tooltip--hover:hover + .lpb-tooltiptext,
+  .is-mercury-edit-mode .lpb-tooltiptext--visible {
+    overflow: visible;
+    clip: auto;
+    width: auto;
+    height: auto;
+  }
+.is-mercury-edit-mode .lpb-tooltiptext::after {
+    left: var(--me-lpb-tooltip-text-arrow-left, 20px);
+  }
 
 @keyframes controlsSlideOpen {
   0% {
diff --git a/build/css/menu.css b/build/css/menu.css
index 100a7aa14782a57b0d854f5829280d78e0ce30fb..1b2cd21dcd2bbef90404ce6e9c59948d11d4351d 100644
--- a/build/css/menu.css
+++ b/build/css/menu.css
@@ -88,3 +88,6 @@
 .lpb-component-list__item.type-card a::before {
     background: url('../../images/menu-icons/icon-card.png');
   }
+.lpb-component-list__item.type-tabs a::before {
+    background: url('../../images/menu-icons/icon-tabs.png');
+  }
diff --git a/build/js/component-form.js b/build/js/component-form.js
index cc0b128c1835e333b64f72f284672bf5ce8efb6d..3abce73eb09dfe97e7c758ec178f03adf2392476 100644
--- a/build/js/component-form.js
+++ b/build/js/component-form.js
@@ -13,7 +13,7 @@
             layoutSelect.focus();
           }
         }
-        const form = once('me-component-form', '.layout-paragraphs-component-form')[0];
+        const form = once('me-component-form', 'mercury-dialog .layout-paragraphs-component-form')[0];
         if (form) {
           form.closest('mercury-dialog').addEventListener('open', (e) => {
             const dialog = e.target.shadowRoot.querySelector('dialog');
diff --git a/build/js/component-form.min.js b/build/js/component-form.min.js
index cb8f753928a3b283a78c48ea58db1326d43d7071..652d4840e3330c9fc90af71101bab771e51eda9b 100644
--- a/build/js/component-form.min.js
+++ b/build/js/component-form.min.js
@@ -1 +1 @@
-!function(){"use strict";((t,e)=>{t.behaviors.mercuryEditorComponentForm={attach:function(t,o){if(t.classList.contains("layout-paragraphs-component-form")){const e=t.querySelector('.layout-select input[type="radio"]:checked');e&&e.focus()}const r=e("me-component-form",".layout-paragraphs-component-form")[0];r&&r.closest("mercury-dialog").addEventListener("open",(t=>{const e=t.target.shadowRoot.querySelector("dialog"),o=e.offsetWidth+"px",r=e.offsetHeight+"px";t.target.style.setProperty("--me-dialog-width-default",o),t.target.style.setProperty("--me-dialog-height-default",r)}))}}})(Drupal,once)}();
+!function(){"use strict";((t,e)=>{t.behaviors.mercuryEditorComponentForm={attach:function(t,o){if(t.classList.contains("layout-paragraphs-component-form")){const e=t.querySelector('.layout-select input[type="radio"]:checked');e&&e.focus()}const r=e("me-component-form","mercury-dialog .layout-paragraphs-component-form")[0];r&&r.closest("mercury-dialog").addEventListener("open",(t=>{const e=t.target.shadowRoot.querySelector("dialog"),o=e.offsetWidth+"px",r=e.offsetHeight+"px";t.target.style.setProperty("--me-dialog-width-default",o),t.target.style.setProperty("--me-dialog-height-default",r)}))}}})(Drupal,once)}();
diff --git a/build/js/edit-screen.js b/build/js/edit-screen.js
index bb012f8355d7a1d12e1f878cb6749c85fe68a515..3214d6f3a3235de1d0739fdc4070914223c2de3e 100644
--- a/build/js/edit-screen.js
+++ b/build/js/edit-screen.js
@@ -116,7 +116,6 @@
       const sidebarToggle = document.querySelector('#me-sidebar-toggle-btn');
       sidebarToggle.addEventListener('click', (e) => {
         e.currentTarget;
-        console.warn('sidebarToggle', sidebarState);
         if (sidebarState === 'open') {
           // When closing the sidebar, set the width to 10px.
           document.documentElement.style.setProperty('--me-dialog-dock-width', '10px');
@@ -145,6 +144,7 @@
           sidebarToggle.classList.remove('me-button--sidebar-expand');
           sidebarToggle.classList.add('me-button--sidebar-collapse');
           sidebarToggle.innerHTML = `<span>${Drupal.t('Hide sidebar')}</span>`;
+          sidebarToggle.setAttribute('title', Drupal.t('Hide sidebar'));
           localStorage.removeItem('mercury-dialog-dock-collapsed');
         }
         else {
@@ -152,6 +152,7 @@
           sidebarToggle.classList.remove('me-button--sidebar-collapse');
           sidebarToggle.classList.add('me-button--sidebar-expand');
           sidebarToggle.innerHTML = `<span>${Drupal.t('Show sidebar')}</span>`;
+          sidebarToggle.setAttribute('title', Drupal.t('Show sidebar'));
           localStorage.setItem('mercury-dialog-dock-collapsed', 'true');
         }
 
@@ -220,6 +221,11 @@
             document.addEventListener('mouseup', iFramePointerEventsToggle);
           }
         }
+        // Set the iframe URL once other js files have loaded.
+        if (once('me-preview-iframe', '#me-preview', context).length) {
+           const iframe = document.querySelector('#me-preview');
+           iframe.src = iframe.getAttribute('data-src');
+        }
       }
     };
   })(Drupal, drupalSettings, jQuery, once);
diff --git a/build/js/edit-screen.min.js b/build/js/edit-screen.min.js
index abd6f63de9ebfacb9ad02b81e38ea3e7670c1cca..d3e83410875a42890496ce3e5b12c2c545e88954 100644
--- a/build/js/edit-screen.min.js
+++ b/build/js/edit-screen.min.js
@@ -1 +1 @@
-!function(){"use strict";((e,t,o,r)=>{let n,i="open";function a(e,t){const o=document.querySelector("#me-preview");e?o.style.width=e:o.style.removeProperty("width"),t?o.style.height=t:o.style.removeProperty("height")}function d(e){const t=document.querySelector('[data-drupal-selector="edit-submit"]:not([disabled])');if(t){const e=t.closest("form").querySelectorAll("input, textarea, select")||[],o=Array.from(e).filter((e=>!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)&&!e.validity.valid));o.length?(o[0].focus(),o[0].reportValidity()):t.dispatchEvent(new Event("mousedown"))}}function l(e){const t=(document.querySelector(".me-edit-screen-redirect-url")??{}).value;return window.location.href=t,!1}function c(e){const t=document.querySelector("#me-preview");t&&(t.style.pointerEvents="mouseup"==e.type?"auto":"none")}o(window).on("dialog:afterclose",((e,t,o)=>{"MERCURY-DIALOG"==(o[0]||{}).tagName&&document.getElementById("me-preview").contentWindow.postMessage({type:"onCloseMercuryDialog"})})),e.behaviors.mercuryEditorEditScreen={attach:function(o,s){const m=r("me-first-error",".js-form-item.error",o)[0];if(m&&(m.focus(),m.scrollIntoView({behavior:"smooth"})),r("me-toolbar","#me-toolbar",o).length&&function(){function o(){const e=document.querySelector(".me-mobile-presets");if(e){const t=e.options[e.selectedIndex??0].value.split("x");a(t[0]+"px",Math.min(t[1],window.innerHeight-document.getElementById("me-toolbar").offsetHeight-20)+"px")}else a("390px",Math.min("844",window.innerHeight-document.getElementById("me-toolbar").offsetHeight-20)+"px")}const r=document.querySelector(".me-mobile-presets");r&&r.addEventListener("change",o),document.querySelector("#me-mobile-toggle-btn").addEventListener("click",(e=>(r&&(r.style.display="block"),o(),window.addEventListener("resize",o),e.preventDefault(),e.stopPropagation(),!1))),document.querySelector("#me-desktop-toggle-btn").addEventListener("click",(e=>(r&&(r.style.display="none"),window.removeEventListener("resize",o),a("100%","100%"),e.preventDefault(),e.stopPropagation(),!1))),document.querySelector('[data-drupal-selector="edit-submit"]:not([disabled])')?document.querySelector("#me-save-btn").addEventListener("click",d):document.querySelector("#me-save-btn").remove(),document.querySelector("#me-done-btn").addEventListener("click",l),t.mercuryEditor&&t.mercuryEditor.width&&localStorage.setItem("mercury-dialog-dock-default-width",t.mercuryEditor.width);let c="true"===localStorage.getItem("mercury-dialog-dock-collapsed");i=c?"closed":"open";const s=document.querySelector("#me-sidebar-toggle-btn");s.addEventListener("click",(e=>(e.currentTarget,console.warn("sidebarToggle",i),"open"===i?(document.documentElement.style.setProperty("--me-dialog-dock-width","10px"),localStorage.setItem("mercury-dialog-dock-collapsed","true")):(n=localStorage.getItem("mercury-dialog-dock-default-width"),n&&document.documentElement.style.setProperty("--me-dialog-dock-width",`${n}px`)),e.preventDefault(),e.stopPropagation(),!1))),document.addEventListener("mercury:dockResize",(t=>{let o=t.detail.width;o>10?(i="open",s.classList.remove("me-button--sidebar-expand"),s.classList.add("me-button--sidebar-collapse"),s.innerHTML=`<span>${e.t("Hide sidebar")}</span>`,localStorage.removeItem("mercury-dialog-dock-collapsed")):(i="closed",s.classList.remove("me-button--sidebar-collapse"),s.classList.add("me-button--sidebar-expand"),s.innerHTML=`<span>${e.t("Show sidebar")}</span>`,localStorage.setItem("mercury-dialog-dock-collapsed","true")),localStorage.setItem("mercury-dialog-dock-width",o)}))}(),r("me-edit-tray","#me-edit-screen",o).length){const r=o.querySelector("#me-edit-screen");if(r){e.mercuryDialog(r).show(),void 0!==e.Ajax&&void 0===e.Ajax.prototype.beforeSerializeMercuryEditor&&(e.Ajax.prototype.beforeSerializeMercuryEditor=e.Ajax.prototype.beforeSerialize,e.Ajax.prototype.beforeSerialize=function(e,o){this.beforeSerializeMercuryEditor.apply(this,arguments);const r=t.ajaxPreviewPageState||{};o.data["ajax_preview_page_state[theme]"]=r.theme,o.data["ajax_preview_page_state[theme_token]"]=r.theme_token,o.data["ajax_preview_page_state[libraries]"]=r.libraries}),document.addEventListener("mousedown",c),document.addEventListener("mouseup",c)}}}}})(Drupal,drupalSettings,jQuery,once)}();
+!function(){"use strict";((e,t,o,r)=>{let i,n="open";function a(e,t){const o=document.querySelector("#me-preview");e?o.style.width=e:o.style.removeProperty("width"),t?o.style.height=t:o.style.removeProperty("height")}function d(e){const t=document.querySelector('[data-drupal-selector="edit-submit"]:not([disabled])');if(t){const e=t.closest("form").querySelectorAll("input, textarea, select")||[],o=Array.from(e).filter((e=>!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)&&!e.validity.valid));o.length?(o[0].focus(),o[0].reportValidity()):t.dispatchEvent(new Event("mousedown"))}}function c(e){const t=(document.querySelector(".me-edit-screen-redirect-url")??{}).value;return window.location.href=t,!1}function l(e){const t=document.querySelector("#me-preview");t&&(t.style.pointerEvents="mouseup"==e.type?"auto":"none")}o(window).on("dialog:afterclose",((e,t,o)=>{"MERCURY-DIALOG"==(o[0]||{}).tagName&&document.getElementById("me-preview").contentWindow.postMessage({type:"onCloseMercuryDialog"})})),e.behaviors.mercuryEditorEditScreen={attach:function(o,s){const m=r("me-first-error",".js-form-item.error",o)[0];if(m&&(m.focus(),m.scrollIntoView({behavior:"smooth"})),r("me-toolbar","#me-toolbar",o).length&&function(){function o(){const e=document.querySelector(".me-mobile-presets");if(e){const t=e.options[e.selectedIndex??0].value.split("x");a(t[0]+"px",Math.min(t[1],window.innerHeight-document.getElementById("me-toolbar").offsetHeight-20)+"px")}else a("390px",Math.min("844",window.innerHeight-document.getElementById("me-toolbar").offsetHeight-20)+"px")}const r=document.querySelector(".me-mobile-presets");r&&r.addEventListener("change",o),document.querySelector("#me-mobile-toggle-btn").addEventListener("click",(e=>(r&&(r.style.display="block"),o(),window.addEventListener("resize",o),e.preventDefault(),e.stopPropagation(),!1))),document.querySelector("#me-desktop-toggle-btn").addEventListener("click",(e=>(r&&(r.style.display="none"),window.removeEventListener("resize",o),a("100%","100%"),e.preventDefault(),e.stopPropagation(),!1))),document.querySelector('[data-drupal-selector="edit-submit"]:not([disabled])')?document.querySelector("#me-save-btn").addEventListener("click",d):document.querySelector("#me-save-btn").remove(),document.querySelector("#me-done-btn").addEventListener("click",c),t.mercuryEditor&&t.mercuryEditor.width&&localStorage.setItem("mercury-dialog-dock-default-width",t.mercuryEditor.width);let l="true"===localStorage.getItem("mercury-dialog-dock-collapsed");n=l?"closed":"open";const s=document.querySelector("#me-sidebar-toggle-btn");s.addEventListener("click",(e=>(e.currentTarget,"open"===n?(document.documentElement.style.setProperty("--me-dialog-dock-width","10px"),localStorage.setItem("mercury-dialog-dock-collapsed","true")):(i=localStorage.getItem("mercury-dialog-dock-default-width"),i&&document.documentElement.style.setProperty("--me-dialog-dock-width",`${i}px`)),e.preventDefault(),e.stopPropagation(),!1))),document.addEventListener("mercury:dockResize",(t=>{let o=t.detail.width;o>10?(n="open",s.classList.remove("me-button--sidebar-expand"),s.classList.add("me-button--sidebar-collapse"),s.innerHTML=`<span>${e.t("Hide sidebar")}</span>`,s.setAttribute("title",e.t("Hide sidebar")),localStorage.removeItem("mercury-dialog-dock-collapsed")):(n="closed",s.classList.remove("me-button--sidebar-collapse"),s.classList.add("me-button--sidebar-expand"),s.innerHTML=`<span>${e.t("Show sidebar")}</span>`,s.setAttribute("title",e.t("Show sidebar")),localStorage.setItem("mercury-dialog-dock-collapsed","true")),localStorage.setItem("mercury-dialog-dock-width",o)}))}(),r("me-edit-tray","#me-edit-screen",o).length){const r=o.querySelector("#me-edit-screen");if(r){e.mercuryDialog(r).show(),void 0!==e.Ajax&&void 0===e.Ajax.prototype.beforeSerializeMercuryEditor&&(e.Ajax.prototype.beforeSerializeMercuryEditor=e.Ajax.prototype.beforeSerialize,e.Ajax.prototype.beforeSerialize=function(e,o){this.beforeSerializeMercuryEditor.apply(this,arguments);const r=t.ajaxPreviewPageState||{};o.data["ajax_preview_page_state[theme]"]=r.theme,o.data["ajax_preview_page_state[theme_token]"]=r.theme_token,o.data["ajax_preview_page_state[libraries]"]=r.libraries}),document.addEventListener("mousedown",l),document.addEventListener("mouseup",l)}}if(r("me-preview-iframe","#me-preview",o).length){const e=document.querySelector("#me-preview");e.src=e.getAttribute("data-src")}}}})(Drupal,drupalSettings,jQuery,once)}();
diff --git a/build/js/horizontal-tabs.js b/build/js/horizontal-tabs.js
index 9d3c6205c108f366104e20a54a22ec52dc21b248..3addb9d0d03d6f6d634ee42f083328ecd19ca50d 100644
--- a/build/js/horizontal-tabs.js
+++ b/build/js/horizontal-tabs.js
@@ -24,6 +24,17 @@
           const selected = tabs.querySelector('input[type="radio"]:checked') || {};
           toggleTabs(selected.value);
         });
+        if (document.querySelector('.me-tab-group .error')) {
+          const tabGroup = document.querySelector('.me-tab-group .error').closest('.me-tab-group');
+          // Get the class that starts with "me-tab-group--" and get the part after "--".
+          const selectedValue = tabGroup.className.match(/me-tab-group--([^ ]+)/)[1];
+          // Get the tab radio button with the same value as the tab group.
+          const selected = document.querySelector(`.me-tabs input[type="radio"][value="${selectedValue}"]`);
+          if (selected) {
+            selected.checked = true;
+            toggleTabs(selectedValue);
+          }
+        }
       }
     };
 
diff --git a/build/js/horizontal-tabs.min.js b/build/js/horizontal-tabs.min.js
index 643f04951a95883199bc457d73660e94e36678e2..3298324fbee15fa3a60c0d885f2b50c3738203c4 100644
--- a/build/js/horizontal-tabs.min.js
+++ b/build/js/horizontal-tabs.min.js
@@ -1 +1 @@
-!function(){"use strict";((e,t,a)=>{function i(e){(document.querySelectorAll(".me-tab-group")||[]).forEach((e=>{e.setAttribute("aria-hidden",!0),e.classList.add("hidden-tab")})),(document.querySelectorAll(`.me-tab-group--${e}`)||[]).forEach((e=>{e.removeAttribute("aria-hidden"),e.classList.remove("hidden-tab")}))}t.behaviors.mercuryEditorTabs={attach:function(e,t){a("me-tabs",'.me-tabs input[type="radio"]').forEach((e=>{e.addEventListener("change",(t=>{i(e.value)}))})),document.querySelectorAll(".me-tabs").forEach((e=>{i((e.querySelector('input[type="radio"]:checked')||{}).value)}))}},e(window).on("dialog:aftercreate",((e,t,a)=>{const i=a.attr("id");if(i&&0===i.indexOf("lpb-dialog-")){const e=a.find(".horizontal-tab-radios"),t=a.closest(".ui-dialog").find(".ui-dialog-titlebar");e.length&&t.length&&t.addClass("has-tabs").append(e)}}))})(jQuery,Drupal,once)}();
+!function(){"use strict";((e,t,a)=>{function r(e){(document.querySelectorAll(".me-tab-group")||[]).forEach((e=>{e.setAttribute("aria-hidden",!0),e.classList.add("hidden-tab")})),(document.querySelectorAll(`.me-tab-group--${e}`)||[]).forEach((e=>{e.removeAttribute("aria-hidden"),e.classList.remove("hidden-tab")}))}t.behaviors.mercuryEditorTabs={attach:function(e,t){if(a("me-tabs",'.me-tabs input[type="radio"]').forEach((e=>{e.addEventListener("change",(t=>{r(e.value)}))})),document.querySelectorAll(".me-tabs").forEach((e=>{r((e.querySelector('input[type="radio"]:checked')||{}).value)})),document.querySelector(".me-tab-group .error")){const e=document.querySelector(".me-tab-group .error").closest(".me-tab-group").className.match(/me-tab-group--([^ ]+)/)[1],t=document.querySelector(`.me-tabs input[type="radio"][value="${e}"]`);t&&(t.checked=!0,r(e))}}},e(window).on("dialog:aftercreate",((e,t,a)=>{const r=a.attr("id");if(r&&0===r.indexOf("lpb-dialog-")){const e=a.find(".horizontal-tab-radios"),t=a.closest(".ui-dialog").find(".ui-dialog-titlebar");e.length&&t.length&&t.addClass("has-tabs").append(e)}}))})(jQuery,Drupal,once)}();
diff --git a/build/js/me-dragula.js b/build/js/me-dragula.js
deleted file mode 100644
index 4e3ace4ac48e5bf8b0917673aba500878ac66185..0000000000000000000000000000000000000000
--- a/build/js/me-dragula.js
+++ /dev/null
@@ -1,205 +0,0 @@
-(function () {
-  'use strict';
-
-  (($, Drupal, once) => {
-
-    /**
-     * Ensures that all layout paragraphs controls are fully within viewport.
-     *
-     * @param {jQuery} $builder
-     *   The Layout Paragraphs container jQuery object.
-     */
-    function repositionControls($builder) {
-      $builder.find('.lpb-controls').each((i, controls) => {
-        controls.setAttribute('style', controls.getAttribute('data-style'));
-        const bounding = controls.getBoundingClientRect();
-        // Left viewport edge.
-        const l = 0;
-        // Right viewport edge.
-        const r = (window.innerWidth || document.documentElement.clientWidth);
-        // Overlapping left.
-        if (bounding.left < l) {
-          controls.setAttribute('data-style', controls.getAttribute('style'));
-          $(controls).offset({left: 0});
-        }
-        // Overlapping right.
-        if (bounding.right > r) {
-          $(controls).css({right: (bounding.right - r) + 'px'});
-        }
-      });
-    }
-
-    /**
-     * Simplifies drag and drop visual cues to prevent jumpiness.
-     *
-     * The default behavior of the dragula library can create excessive
-     * jumpiness in some cases. This function simplifies the UI and drag and drop
-     * experience in several key ways, including:
-     *
-     * - Detaches all layout paragraphs UI elements when dragging starts.
-     * - Provides a simple "hint" element to show where an item will be dropped.
-     * - Leaves a "ghost" copy of the grabbed element in place at the source.
-     * - Reattaches all UI elements when dragging ends.
-     *
-     * @see https://github.com/bevacqua/dragula#drakeon-events.
-     *
-     * @param {Object} drake
-     *   The dragula object.
-     */
-    function simplifyDragHints($builder, settings) {
-
-      const drake = $builder.data('drake');
-      let ghost, grabbed;
-      const hint = $('<div class="lp-hint hidden"></div>')[0];
-
-      // Hide UI elements when dragging starts.
-      drake.on('drag', (el) => {
-        el.parentNode.insertBefore(hint, el);
-        $builder.find('.js-lpb-ui').addClass('hidden');
-      });
-      // Provide a simple hint element to indicate where an item will be dropped.
-      drake.on('shadow', (item, container) => {
-        if (item.classList.contains('lp-hint')) {
-          return;
-        }
-        // Remove comments and text nodes from container.
-        [...container.childNodes].filter((e) => e.classList === undefined).forEach((e) => e.remove());
-        container.replaceChild(hint, item);
-
-        // Ensure the hint does not get at the end of the region after the add button.
-        if (hint.nextSibling === null && hint.previousSibling !== null && hint.previousSibling.classList.contains('lpb-btn--add')) {
-          container.insertBefore(hint, hint.previousSibling);
-        }
-
-        const nextIsGhost = hint.nextSibling !== null ? hint.nextSibling.classList.contains('lp-ghost') : false;
-        const prevIsGhost = hint.previousSibling !== null ? hint.previousSibling.classList.contains('lp-ghost') : false;
-        const ghostAdjacent = nextIsGhost || prevIsGhost;
-        if (ghostAdjacent) {
-          hint.classList.add('hidden');
-          ghost.classList.remove('gu-transit');
-        }
-        else {
-          hint.classList.remove('hidden');
-          ghost.classList.add('gu-transit');
-        }
-      });
-      // Leave a copy of the grabbed item in place at the original source.
-      drake.on('cloned', (mirror, item) => {
-        ghost = item.cloneNode(true);
-        ghost.classList.add('lp-ghost');
-        item.parentNode.insertBefore(ghost, item);
-        grabbed = item;
-        item.remove();
-      });
-      // Show UI elements and remove ghost and hint elements when dragging stops.
-      drake.on('dragend', (el) => {
-        hint.replaceWith(grabbed);
-        ghost.remove();
-        $builder.find('.js-lpb-ui').removeClass('hidden');
-        repositionControls($builder);
-        $builder.trigger('lpb-component:drop', [el.getAttribute('data-uuid')]);
-      });
-    }
-
-    /**
-     * Waits for a condition to be met, then calls the provided callback.
-     *
-     * @param {Function} cond
-     *   The condition to wait for.
-     * @param {Function} cb
-     *   The callback to call when cond evaluates true.
-     */
-    function waitFor(cond, cb) {
-      const i = setInterval(() => {
-        if (cond() === true) {
-          clearInterval(i);
-          cb();
-        }
-      }, 100);
-    }
-
-    function removeMovedFormActions() {
-      const outdatedFormActions = document.getElementsByClassName('lpb-form__actions repositioned');
-      if (outdatedFormActions.length) {
-        outdatedFormActions[0].remove();
-      }
-    }
-
-    function moveFormActions(context) {
-      const formActions = context.querySelector('.lpb-form__actions');
-      if (formActions) {
-        removeMovedFormActions();
-        formActions.classList.add('repositioned');
-        document.body.appendChild(formActions);
-      }
-    }
-
-    function showControls(e) {
-      const el = e.target.closest('.lpb-controls, .js-lpb-component');
-      el.classList.add('focused');
-      el.classList.remove('transitioning');
-      el.classList.remove('blurred');
-    }
-
-    function hideControls(e) {
-      const el = e.target.closest('.lpb-controls, .js-lpb-component');
-      el.classList.add('transitioning');
-      setTimeout(() => {
-        if (el.classList.contains('transitioning')) {
-          el.classList.remove('focused');
-          el.classList.remove('transitioning');
-          el.classList.add('blurred');
-        }
-      }, 250);
-    }
-
-    Drupal.behaviors.mercuryEditorDragula = {
-      attach: function attach(context, settings) {
-        // Append form-actions to body for better styling control.
-        if (once('me-builder-events', 'html', context).length) {
-          $(window).on('lpb-builder:open.lpb lpb-builder:save.lpb', (e) => {
-            moveFormActions(context);
-          });
-          $(window).on('lpb-builder:close.lpb', (e) => {
-            removeMovedFormActions();
-          });
-        }
-        var events = ['lpb-component:insert.lpb', 'lpb-component:update.lpb', 'lpb-component:move.lpb', 'lpb-component:drop.lpb'].join(' ');
-        $(once('me-builder-form', '[data-lpb-id]', context)).on(events, function (e) {
-          const cancelButton = document.querySelector('lpb-form__actions repositioned .lpb-btn--cancel');
-          if (cancelButton) {
-            cancelButton.value = Drupal.t('Cancel');
-          }
-        });
-        once('me-dragula', '.lp-builder.has-components').forEach((builder) => {
-          const $builder = $(builder);
-          waitFor(
-            () => $builder.data('drake') !== undefined,
-            () => {
-              repositionControls($builder);
-              moveFormActions($builder[0]);
-              $(window).resize(() => repositionControls($builder));
-              // Simplifies drag and drop functionality.
-              try {
-                simplifyDragHints($builder, settings);
-              }
-              catch (e) {
-                console.warn(e);
-              }
-            });
-        });
-        once('reveal-on-hover', '.js-lpb-component').forEach((component) => {
-          component.addEventListener('mouseenter', showControls);
-          component.addEventListener('mouseleave', hideControls);
-        });
-        once('reveal-on-hover', '.lpb-controls').forEach((el) => {
-          el.addEventListener('mouseenter', showControls);
-          el.addEventListener("focusin", showControls);
-          el.addEventListener('mouseleave', hideControls);
-          el.addEventListener("focusout", hideControls);
-        });
-      }
-    };
-  })(jQuery, Drupal, once);
-
-})();
diff --git a/build/js/me-dragula.min.js b/build/js/me-dragula.min.js
deleted file mode 100644
index ffe05c7b8da9c2f24f5d2a0d716901a499c8a56d..0000000000000000000000000000000000000000
--- a/build/js/me-dragula.min.js
+++ /dev/null
@@ -1 +0,0 @@
-!function(){"use strict";((e,t,n)=>{function s(t){t.find(".lpb-controls").each(((t,n)=>{n.setAttribute("style",n.getAttribute("data-style"));const s=n.getBoundingClientRect(),o=window.innerWidth||document.documentElement.clientWidth;s.left<0&&(n.setAttribute("data-style",n.getAttribute("style")),e(n).offset({left:0})),s.right>o&&e(n).css({right:s.right-o+"px"})}))}function o(){const e=document.getElementsByClassName("lpb-form__actions repositioned");e.length&&e[0].remove()}function i(e){const t=e.querySelector(".lpb-form__actions");t&&(o(),t.classList.add("repositioned"),document.body.appendChild(t))}function l(e){const t=e.target.closest(".lpb-controls, .js-lpb-component");t.classList.add("focused"),t.classList.remove("transitioning"),t.classList.remove("blurred")}function r(e){const t=e.target.closest(".lpb-controls, .js-lpb-component");t.classList.add("transitioning"),setTimeout((()=>{t.classList.contains("transitioning")&&(t.classList.remove("focused"),t.classList.remove("transitioning"),t.classList.add("blurred"))}),250)}t.behaviors.mercuryEditorDragula={attach:function(a,d){n("me-builder-events","html",a).length&&(e(window).on("lpb-builder:open.lpb lpb-builder:save.lpb",(e=>{i(a)})),e(window).on("lpb-builder:close.lpb",(e=>{o()})));var c=["lpb-component:insert.lpb","lpb-component:update.lpb","lpb-component:move.lpb","lpb-component:drop.lpb"].join(" ");e(n("me-builder-form","[data-lpb-id]",a)).on(c,(function(e){const n=document.querySelector("lpb-form__actions repositioned .lpb-btn--cancel");n&&(n.value=t.t("Cancel"))})),n("me-dragula",".lp-builder.has-components").forEach((t=>{const n=e(t);!function(e,t){const n=setInterval((()=>{!0===e()&&(clearInterval(n),t())}),100)}((()=>void 0!==n.data("drake")),(()=>{s(n),i(n[0]),e(window).resize((()=>s(n)));try{!function(t,n){const o=t.data("drake");let i,l;const r=e('<div class="lp-hint hidden"></div>')[0];o.on("drag",(e=>{e.parentNode.insertBefore(r,e),t.find(".js-lpb-ui").addClass("hidden")})),o.on("shadow",((e,t)=>{if(e.classList.contains("lp-hint"))return;[...t.childNodes].filter((e=>void 0===e.classList)).forEach((e=>e.remove())),t.replaceChild(r,e),null===r.nextSibling&&null!==r.previousSibling&&r.previousSibling.classList.contains("lpb-btn--add")&&t.insertBefore(r,r.previousSibling);const n=null!==r.nextSibling&&r.nextSibling.classList.contains("lp-ghost"),s=null!==r.previousSibling&&r.previousSibling.classList.contains("lp-ghost");n||s?(r.classList.add("hidden"),i.classList.remove("gu-transit")):(r.classList.remove("hidden"),i.classList.add("gu-transit"))})),o.on("cloned",((e,t)=>{i=t.cloneNode(!0),i.classList.add("lp-ghost"),t.parentNode.insertBefore(i,t),l=t,t.remove()})),o.on("dragend",(e=>{r.replaceWith(l),i.remove(),t.find(".js-lpb-ui").removeClass("hidden"),s(t),t.trigger("lpb-component:drop",[e.getAttribute("data-uuid")])}))}(n)}catch(e){console.warn(e)}}))})),n("reveal-on-hover",".js-lpb-component").forEach((e=>{e.addEventListener("mouseenter",l),e.addEventListener("mouseleave",r)})),n("reveal-on-hover",".lpb-controls").forEach((e=>{e.addEventListener("mouseenter",l),e.addEventListener("focusin",l),e.addEventListener("mouseleave",r),e.addEventListener("focusout",r)}))}}})(jQuery,Drupal,once)}();
diff --git a/build/js/post-messages-listener.js b/build/js/post-messages-listener.js
index a90b3eaf364b60df0528db4871b6e468c0cce736..1dd65489b1dfcfa5045c3b3423634d1f24cd2f7d 100644
--- a/build/js/post-messages-listener.js
+++ b/build/js/post-messages-listener.js
@@ -11,14 +11,6 @@
        * @param {Object} settings The ajax settings.
        */
       drupalAjax: function (settings) {
-        console.log('drupalAjax', settings);
-        Drupal.ajax(settings).execute();
-      },
-      /**
-       * Ajax click handler for Layout Paragraphs UI elements in iframe.
-       * @param {Object} settings The ajax settings.
-       */
-      lpbUiClick: function (settings) {
         Drupal.ajax(settings).execute();
       },
       /**
diff --git a/build/js/post-messages-listener.min.js b/build/js/post-messages-listener.min.js
index f889bbe456bc1f05d6b2464030afd9c9393798f3..1f1cf5662471d8da872ff111d76e962526cfca39 100644
--- a/build/js/post-messages-listener.min.js
+++ b/build/js/post-messages-listener.min.js
@@ -1 +1 @@
-!function(){"use strict";((e,t,n)=>{const a={drupalAjax:function(t){console.log("drupalAjax",t),e.ajax(t).execute()},lpbUiClick:function(t){e.ajax(t).execute()},syncChanges:function(e){const{ref:t,value:n}=e;document.querySelector(`[data-sync-changes="${t}"]`).innerHTML=n},ajaxCommands:function(t){const{commands:n,status:a}=t,o=e.ajax({url:""}),s=new e.AjaxCommands;Object.keys(n||{}).reduce((function(e,t){return e.then((function(){var e=n[t].command;if(e&&s[e])return s[e](o,n[t],a)})).catch(console.error)}),Promise.resolve())},ajaxPreviewPageState:function(e){t.ajaxPreviewPageState=e},onCloseMercuryDialog:function(){document.querySelectorAll(".is-me-focused").forEach((e=>{e.focus(),e.classList.remove("is-me-focused")}))}};e.behaviors.mercuryEditorPostMessagesListener={attach:function(e,t){n("me-msg-listener","html").length&&window.addEventListener("message",(e=>{a[e.data.type]&&a[e.data.type](e.data.settings)}))}}})(Drupal,drupalSettings,once)}();
+!function(){"use strict";((e,t,n)=>{const a={drupalAjax:function(t){e.ajax(t).execute()},syncChanges:function(e){const{ref:t,value:n}=e;document.querySelector(`[data-sync-changes="${t}"]`).innerHTML=n},ajaxCommands:function(t){const{commands:n,status:a}=t,s=e.ajax({url:""}),o=new e.AjaxCommands;Object.keys(n||{}).reduce((function(e,t){return e.then((function(){var e=n[t].command;if(e&&o[e])return o[e](s,n[t],a)})).catch(console.error)}),Promise.resolve())},ajaxPreviewPageState:function(e){t.ajaxPreviewPageState=e},onCloseMercuryDialog:function(){document.querySelectorAll(".is-me-focused").forEach((e=>{e.focus(),e.classList.remove("is-me-focused")}))}};e.behaviors.mercuryEditorPostMessagesListener={attach:function(e,t){n("me-msg-listener","html").length&&window.addEventListener("message",(e=>{a[e.data.type]&&a[e.data.type](e.data.settings)}))}}})(Drupal,drupalSettings,once)}();
diff --git a/build/js/preview-screen.js b/build/js/preview-screen.js
index 917b799c9bc49d9be642f24bdd5e17b1c6da2e53..0029166f5befd28ab2303025e1dfa4bc8248c3ea 100644
--- a/build/js/preview-screen.js
+++ b/build/js/preview-screen.js
@@ -1,7 +1,7 @@
 (function () {
   'use strict';
 
-  ((Drupal, drupalSettings, once) => {
+  ((Drupal, drupalSettings, $, once) => {
 
     /**
      * Prevent a click.
@@ -32,7 +32,7 @@
       e.currentTarget.classList.add('is-me-focused');
       // Then, send the click event.
       const message = {
-        type: 'lpbUiClick',
+        type: 'drupalAjax',
         settings: {
           dialogType: e.currentTarget.getAttribute('data-dialog-type'),
           dialog: JSON.parse(e.currentTarget.getAttribute('data-dialog-options')),
@@ -46,18 +46,202 @@
       return false;
     }
 
+    function scaleMirror(e) {
+      const mirror = document.querySelector('.gu-mirror');
+      if (!mirror) {
+        return;
+      }
+      const scaleAxis = mirror.offsetWidth > mirror.offsetHeight ? 'offsetHeight' : 'offsetWidth';
+      const scale = Math.min(300 / mirror[scaleAxis], 1);
+      const boundingRect = mirror.getBoundingClientRect();
+      const diffX = e.clientX - boundingRect.x;
+      const diffY = e.clientY - boundingRect.y;
+      mirror.style.setProperty('transform-origin', `${diffX}px ${diffY}px`);
+      mirror.style.setProperty('transform', `scale(${scale})`);
+      window.removeEventListener('mousemove', scaleMirror);
+    }
+
+    /**
+     * Simplifies drag and drop visual cues to prevent jumpiness.
+     *
+     * The default behavior of the dragula library can create excessive
+     * jumpiness in some cases. This function simplifies the UI and drag and drop
+     * experience in several key ways, including:
+     *
+     * - Detaches all layout paragraphs UI elements when dragging starts.
+     * - Provides a simple "hint" element to show where an item will be dropped.
+     * - Leaves a "ghost" copy of the grabbed element in place at the source.
+     * - Reattaches all UI elements when dragging ends.
+     *
+     * @see https://github.com/bevacqua/dragula#drakeon-events.
+     *
+     * @param {Object} drake
+     *   The dragula object.
+     */
+    function simplifyDragHints($builder, drake) {
+
+      let ghost, grabbed;
+      const hint = $('<div class="lp-hint hidden"></div>')[0];
+      // Scales the mirror element to make it easier to drag.
+      drake.on('cloned', (clone, original, type) => {
+        window.addEventListener('mousemove', scaleMirror);
+      });
+      // Hide UI elements when dragging starts.
+      drake.on('drag', (el) => {
+        if (el.parentNode) {
+          el.parentNode.insertBefore(hint, el);
+        }
+        $builder.find('.js-lpb-ui').addClass('hidden');
+      });
+      // Provide a simple hint element to indicate where an item will be dropped.
+      drake.on('shadow', (shadow, container, src) => {
+
+        if (shadow.classList.contains('lp-hint')) {
+          return;
+        }
+
+        hint.style = {
+          width: '',
+          height: '',
+          marginLeft: '',
+          marginTop: '',
+        };
+
+        const sibling = shadow.nextElementSibling || shadow.previousElementSibling;
+        const orientation = sibling && shadow.getBoundingClientRect().top === sibling.getBoundingClientRect().top
+            ? 'vertical'
+            : 'horizontal';
+
+        if (orientation == 'horizontal') {
+          const offset = parseInt(window.getComputedStyle(shadow.parentNode).getPropertyValue('padding-left'));
+          hint.style.marginLeft = '-' + offset + 'px';
+        }
+
+        if (orientation == 'vertical') {
+          const offset = parseInt(window.getComputedStyle(shadow.parentNode).getPropertyValue('padding-top'));
+          hint.style.marginTop = '-' + offset + 'px';
+        }
+
+        if (orientation === 'vertical') {
+          hint.style.height = shadow.parentNode.clientHeight + 'px';
+        }
+        if (orientation === 'horizontal') {
+          hint.style.width = shadow.parentNode.clientWidth + 'px';
+        }
+
+        hint.setAttribute('data-orientation', orientation);
+
+        // Remove comments and text nodes from container.
+        [...container.childNodes].filter((e) => e.classList === undefined).forEach((e) => e.remove());
+        container.replaceChild(hint, shadow);
+
+        // Ensure the hint does not get at the end of the region after the add button.
+        if (hint.nextSibling === null && hint.previousSibling !== null && hint.previousSibling.classList.contains('lpb-btn--add')) {
+          container.insertBefore(hint, hint.previousSibling);
+        }
+
+        const nextIsGhost = hint.nextSibling !== null ? hint.nextSibling.classList.contains('lp-ghost') : false;
+        const prevIsGhost = hint.previousSibling !== null ? hint.previousSibling.classList.contains('lp-ghost') : false;
+        const ghostAdjacent = nextIsGhost || prevIsGhost;
+        if (ghostAdjacent) {
+          hint.classList.add('hidden');
+          ghost.classList.remove('gu-transit');
+        }
+        else {
+          hint.classList.remove('hidden');
+          ghost.classList.add('gu-transit');
+        }
+      });
+      // Leave a copy of the grabbed item in place at the original source.
+      drake.on('cloned', (mirror, item) => {
+        ghost = item.cloneNode(true);
+        ghost.classList.add('lp-ghost');
+        if (item.parentNode) {
+          item.parentNode.insertBefore(ghost, item);
+        }
+        grabbed = item;
+        item.remove();
+      });
+      // Show UI elements and remove ghost and hint elements when dragging stops.
+      drake.on('dragend', (el) => {
+        hint.replaceWith(grabbed);
+        ghost.remove();
+        $builder.find('.js-lpb-ui').removeClass('hidden');
+      });
+    }
+
+    /**
+     * Calls simplifyDragHints() when the builder is initialized.
+     */
+    $(document).on('lpb-builder:init', (e) => {
+      const builder = e.target;
+      const drake = $(builder).data('drake');
+      if (drake) {
+        simplifyDragHints($(builder), drake);
+      }
+    });
+
+    function padElements(layout) {
+      [...layout.querySelectorAll('[data-region]'), layout].forEach((el) => {
+        const computed = getComputedStyle(el);
+        if (!el.hasAttribute('data-me-padding')) {
+          el.setAttribute('data-me-padding', `${computed.paddingTop} ${computed.paddingRight} ${computed.paddingBottom} ${computed.paddingLeft}`);
+        }
+        el.style.paddingTop = Math.max(10, parseInt(computed.paddingTop)) + 'px';
+        el.style.paddingRight = Math.max(10, parseInt(computed.paddingRight)) + 'px';
+        el.style.paddingBottom = Math.max(10, parseInt(computed.paddingBottom)) + 'px';
+        el.style.paddingLeft = Math.max(10, parseInt(computed.paddingLeft)) + 'px';
+      });
+    }
+
+    function unpadElements(layout) {
+      [...layout.querySelectorAll('[data-region]'), layout].forEach((el) => {
+        if (el.hasAttribute('data-me-padding')) {
+          getComputedStyle(el);
+          el.style.padding = el.getAttribute('data-me-padding');
+        }
+      });
+    }
+
+    function showControls(e) {
+      const el = e.target.closest('.lpb-controls, .js-lpb-component');
+      el.classList.add('focused');
+      el.classList.remove('transitioning');
+      el.classList.remove('blurred');
+    }
+
+    function hideControls(e) {
+      const el = e.target.closest('.lpb-controls, .js-lpb-component');
+      el.classList.add('transitioning');
+      setTimeout(() => {
+        if (el.classList.contains('transitioning')) {
+          el.classList.remove('focused');
+          el.classList.remove('transitioning');
+          el.classList.add('blurred');
+        }
+      }, 250);
+    }
+
     /**
      * Attaches the behavior to the edit screen.
      */
     Drupal.behaviors.mercuryEditorPreviewScreen = {
       attach: function(context, _settings) {
+        const duplicateContainers = [...document.querySelectorAll('[data-me-edit-screen-key]')]
+          .map((container) => container.getAttribute('data-me-edit-screen-key'))
+          // check for duplicates in array
+          .filter((value, index, self) => self.indexOf(value) !== index);
+        if (duplicateContainers.length > 0) {
+          console.error('Multiple HTML elements found using the same data attribute, "data-me-edit-screen-key", which should be unique. Make sure attributes are not passed to child elements in twig templates.', duplicateContainers);
+        }
         // Send the initial ajaxPageState to the parent window.
         window.parent.postMessage({
           type: 'ajaxPreviewPageState',
           settings: drupalSettings.ajaxPageState
         });
         // Attaches click handlers to links that use window.postMessage().
-        once('me-msg-broadcaster', '.use-postmessage').forEach((el) => {
+        once('me-msg-broadcaster', '.js-lpb-ui.use-ajax, .js-lpb-ui .use-ajax').forEach((el) => {
+          $(el).off();
           el.addEventListener('mousedown', preventDefault);
           el.addEventListener('mouseup', preventDefault);
           el.addEventListener('click', lpbClickHander);
@@ -73,18 +257,52 @@
                 return false;
               });
             }
+            else {
+              // The dragula library prevents links from automatically focusing
+              // on mousedown, which can cause issues with keyboard navigation.
+              link.addEventListener('mousedown', (e) => e.target.focus());
+            }
           });
           once('me-prevent-focus', 'a, button, input, textarea, select, details', context).forEach((focussable) => {
             if (
               focussable.closest('.lpb-controls') === null &&
+              focussable.closest('.mercury-editor-ui') === null &&
               !focussable.classList.contains('use-postmessage')
             ) {
               focussable.setAttribute('tabindex', '-1');
             }
           });
+          once('me-layout-hover', '.lpb-layout').forEach((layout) => {
+            layout.addEventListener('mouseenter', (e) => {
+              e.target.setAttribute('data-mouseover', 'true');
+              setTimeout(() => {
+                if (e.target.getAttribute('data-mouseover')) {
+                  padElements(e.target);
+                }
+              }, 100);
+            });
+            layout.addEventListener('mouseleave', (e) => {
+              e.target.removeAttribute('data-mouseover');
+              setTimeout(() => {
+                if (!e.target.getAttribute('data-mouseover')) {
+                  unpadElements(e.target);
+                }
+              }, 100);
+            });
+          });
         }
+        once('reveal-on-hover', '.js-lpb-component').forEach((component) => {
+          component.addEventListener('mouseenter', showControls);
+          component.addEventListener('mouseleave', hideControls);
+        });
+        once('reveal-on-hover', '.lpb-controls').forEach((el) => {
+          el.addEventListener('mouseenter', showControls);
+          el.addEventListener("focusin", showControls);
+          el.addEventListener('mouseleave', hideControls);
+          el.addEventListener("focusout", hideControls);
+        });
       }
     };
-  })(Drupal, drupalSettings, once);
+  })(Drupal, drupalSettings, jQuery, once);
 
 })();
diff --git a/build/js/preview-screen.min.js b/build/js/preview-screen.min.js
index 29199f19cba2917828d67185888f2e2e39f4ba44..c7391af321c02adeff15dfb5273a0d43f2a4da55 100644
--- a/build/js/preview-screen.min.js
+++ b/build/js/preview-screen.min.js
@@ -1 +1 @@
-!function(){"use strict";((e,t,a)=>{function r(e){return e.stopPropagation(),e.preventDefault(),!1}function s(e){window.parent.postMessage({type:"ajaxPreviewPageState",settings:t.ajaxPageState}),document.querySelectorAll(".is-me-focused").forEach((e=>{e.classList.remove("is-me-focused")})),e.currentTarget.classList.add("is-me-focused");const a={type:"lpbUiClick",settings:{dialogType:e.currentTarget.getAttribute("data-dialog-type"),dialog:JSON.parse(e.currentTarget.getAttribute("data-dialog-options")),dialogRenderer:JSON.parse(e.currentTarget.getAttribute("data-dialog-renderer")),url:e.currentTarget.getAttribute("href")}};return window.parent.postMessage(a),e.stopPropagation(),e.preventDefault(),!1}e.behaviors.mercuryEditorPreviewScreen={attach:function(e,n){window.parent.postMessage({type:"ajaxPreviewPageState",settings:t.ajaxPageState}),a("me-msg-broadcaster",".use-postmessage").forEach((e=>{e.addEventListener("mousedown",r),e.addEventListener("mouseup",r),e.addEventListener("click",s)})),window.parent!==window&&(a("me-stop-iframed-links","a",e).forEach((e=>{null===e.closest(".lpb-controls")&&(e.setAttribute("target","_parent"),e.addEventListener("click",(e=>(e.stopPropagation(),e.preventDefault(),!1))))})),a("me-prevent-focus","a, button, input, textarea, select, details",e).forEach((e=>{null!==e.closest(".lpb-controls")||e.classList.contains("use-postmessage")||e.setAttribute("tabindex","-1")})))}}})(Drupal,drupalSettings,once)}();
+!function(){"use strict";((e,t,n,a)=>{function s(e){return e.stopPropagation(),e.preventDefault(),!1}function i(e){window.parent.postMessage({type:"ajaxPreviewPageState",settings:t.ajaxPageState}),document.querySelectorAll(".is-me-focused").forEach((e=>{e.classList.remove("is-me-focused")})),e.currentTarget.classList.add("is-me-focused");const n={type:"drupalAjax",settings:{dialogType:e.currentTarget.getAttribute("data-dialog-type"),dialog:JSON.parse(e.currentTarget.getAttribute("data-dialog-options")),dialogRenderer:JSON.parse(e.currentTarget.getAttribute("data-dialog-renderer")),url:e.currentTarget.getAttribute("href")}};return window.parent.postMessage(n),e.stopPropagation(),e.preventDefault(),!1}function o(e){const t=document.querySelector(".gu-mirror");if(!t)return;const n=t.offsetWidth>t.offsetHeight?"offsetHeight":"offsetWidth",a=Math.min(300/t[n],1),s=t.getBoundingClientRect(),i=e.clientX-s.x,r=e.clientY-s.y;t.style.setProperty("transform-origin",`${i}px ${r}px`),t.style.setProperty("transform",`scale(${a})`),window.removeEventListener("mousemove",o)}function r(e){const t=e.target.closest(".lpb-controls, .js-lpb-component");t.classList.add("focused"),t.classList.remove("transitioning"),t.classList.remove("blurred")}function d(e){const t=e.target.closest(".lpb-controls, .js-lpb-component");t.classList.add("transitioning"),setTimeout((()=>{t.classList.contains("transitioning")&&(t.classList.remove("focused"),t.classList.remove("transitioning"),t.classList.add("blurred"))}),250)}n(document).on("lpb-builder:init",(e=>{const t=e.target,a=n(t).data("drake");a&&function(e,t){let a,s;const i=n('<div class="lp-hint hidden"></div>')[0];t.on("cloned",((e,t,n)=>{window.addEventListener("mousemove",o)})),t.on("drag",(t=>{t.parentNode&&t.parentNode.insertBefore(i,t),e.find(".js-lpb-ui").addClass("hidden")})),t.on("shadow",((e,t,n)=>{if(e.classList.contains("lp-hint"))return;i.style={width:"",height:"",marginLeft:"",marginTop:""};const s=e.nextElementSibling||e.previousElementSibling,o=s&&e.getBoundingClientRect().top===s.getBoundingClientRect().top?"vertical":"horizontal";if("horizontal"==o){const t=parseInt(window.getComputedStyle(e.parentNode).getPropertyValue("padding-left"));i.style.marginLeft="-"+t+"px"}if("vertical"==o){const t=parseInt(window.getComputedStyle(e.parentNode).getPropertyValue("padding-top"));i.style.marginTop="-"+t+"px"}"vertical"===o&&(i.style.height=e.parentNode.clientHeight+"px"),"horizontal"===o&&(i.style.width=e.parentNode.clientWidth+"px"),i.setAttribute("data-orientation",o),[...t.childNodes].filter((e=>void 0===e.classList)).forEach((e=>e.remove())),t.replaceChild(i,e),null===i.nextSibling&&null!==i.previousSibling&&i.previousSibling.classList.contains("lpb-btn--add")&&t.insertBefore(i,i.previousSibling);const r=null!==i.nextSibling&&i.nextSibling.classList.contains("lp-ghost"),d=null!==i.previousSibling&&i.previousSibling.classList.contains("lp-ghost");r||d?(i.classList.add("hidden"),a.classList.remove("gu-transit")):(i.classList.remove("hidden"),a.classList.add("gu-transit"))})),t.on("cloned",((e,t)=>{a=t.cloneNode(!0),a.classList.add("lp-ghost"),t.parentNode&&t.parentNode.insertBefore(a,t),s=t,t.remove()})),t.on("dragend",(t=>{i.replaceWith(s),a.remove(),e.find(".js-lpb-ui").removeClass("hidden")}))}(n(t),a)})),e.behaviors.mercuryEditorPreviewScreen={attach:function(e,o){const l=[...document.querySelectorAll("[data-me-edit-screen-key]")].map((e=>e.getAttribute("data-me-edit-screen-key"))).filter(((e,t,n)=>n.indexOf(e)!==t));l.length>0&&console.error('Multiple HTML elements found using the same data attribute, "data-me-edit-screen-key", which should be unique. Make sure attributes are not passed to child elements in twig templates.',l),window.parent.postMessage({type:"ajaxPreviewPageState",settings:t.ajaxPageState}),a("me-msg-broadcaster",".js-lpb-ui.use-ajax, .js-lpb-ui .use-ajax").forEach((e=>{n(e).off(),e.addEventListener("mousedown",s),e.addEventListener("mouseup",s),e.addEventListener("click",i)})),window.parent!==window&&(a("me-stop-iframed-links","a",e).forEach((e=>{null===e.closest(".lpb-controls")?(e.setAttribute("target","_parent"),e.addEventListener("click",(e=>(e.stopPropagation(),e.preventDefault(),!1)))):e.addEventListener("mousedown",(e=>e.target.focus()))})),a("me-prevent-focus","a, button, input, textarea, select, details",e).forEach((e=>{null!==e.closest(".lpb-controls")||null!==e.closest(".mercury-editor-ui")||e.classList.contains("use-postmessage")||e.setAttribute("tabindex","-1")})),a("me-layout-hover",".lpb-layout").forEach((e=>{e.addEventListener("mouseenter",(e=>{e.target.setAttribute("data-mouseover","true"),setTimeout((()=>{e.target.getAttribute("data-mouseover")&&function(e){[...e.querySelectorAll("[data-region]"),e].forEach((e=>{const t=getComputedStyle(e);e.hasAttribute("data-me-padding")||e.setAttribute("data-me-padding",`${t.paddingTop} ${t.paddingRight} ${t.paddingBottom} ${t.paddingLeft}`),e.style.paddingTop=Math.max(10,parseInt(t.paddingTop))+"px",e.style.paddingRight=Math.max(10,parseInt(t.paddingRight))+"px",e.style.paddingBottom=Math.max(10,parseInt(t.paddingBottom))+"px",e.style.paddingLeft=Math.max(10,parseInt(t.paddingLeft))+"px"}))}(e.target)}),100)})),e.addEventListener("mouseleave",(e=>{e.target.removeAttribute("data-mouseover"),setTimeout((()=>{e.target.getAttribute("data-mouseover")||function(e){[...e.querySelectorAll("[data-region]"),e].forEach((e=>{e.hasAttribute("data-me-padding")&&(getComputedStyle(e),e.style.padding=e.getAttribute("data-me-padding"))}))}(e.target)}),100)}))}))),a("reveal-on-hover",".js-lpb-component").forEach((e=>{e.addEventListener("mouseenter",r),e.addEventListener("mouseleave",d)})),a("reveal-on-hover",".lpb-controls").forEach((e=>{e.addEventListener("mouseenter",r),e.addEventListener("focusin",r),e.addEventListener("mouseleave",d),e.addEventListener("focusout",d)}))}}})(Drupal,drupalSettings,jQuery,once)}();
diff --git a/composer.json b/composer.json
index 4a63a25cff4e1e8b744db29c32f4487cf4dfd175..8c985e5411f65039a67854828ac4c63d45475865 100644
--- a/composer.json
+++ b/composer.json
@@ -25,7 +25,7 @@
     }
   ],
   "require": {
-    "drupal/layout_paragraphs": "^2",
+    "drupal/layout_paragraphs": "^2.1",
     "drupal/style_options": "^1",
     "php": ">=8.1"
   }
diff --git a/images/menu-icons/icon-tabs.png b/images/menu-icons/icon-tabs.png
new file mode 100644
index 0000000000000000000000000000000000000000..3ff407ab501ad6b629a04c94409343a3c00bb782
Binary files /dev/null and b/images/menu-icons/icon-tabs.png differ
diff --git a/mercury_editor.install b/mercury_editor.install
index 443be513a60db5c522a91cea21d66834c776f84e..ae19c603ef509c3278d9859a83fa843813109592 100644
--- a/mercury_editor.install
+++ b/mercury_editor.install
@@ -15,3 +15,13 @@ function mercury_editor_update_9001() {
   }
   $mercury_settings->clear('content_types')->save();
 }
+
+/**
+ * Set dialog_tray_width in mercury_editor.settings config if it does not exist.
+ */
+function mercury_editor_update_9002() {
+  $mercury_settings = Drupal::configFactory()->getEditable('mercury_editor.settings');
+  if (!$mercury_settings->get('dialog_tray_width')) {
+    $mercury_settings->set('dialog_tray_width', '400')->save();
+  }
+}
diff --git a/mercury_editor.libraries.yml b/mercury_editor.libraries.yml
index 2feccc514e44170789ded45dfa67294bb35fc758..5d5ad8d934f559132082184879ef730bb40b4dcf 100755
--- a/mercury_editor.libraries.yml
+++ b/mercury_editor.libraries.yml
@@ -14,8 +14,6 @@ mercury_editor:
       build/css/components/table.css: {}
     theme:
       build/css/theme/theme.css: {}
-  js:
-    build/js/me-dragula.js: {}
   dependencies:
     - mercury_editor/base
     - mercury_editor/unpublished_hint
diff --git a/mercury_editor.module b/mercury_editor.module
index 362b6ab9e4c02b7fe95e687be4cdb9d788e36da0..9e54d91a8afd12d1cd8d09513503aec10f64e0cf 100755
--- a/mercury_editor.module
+++ b/mercury_editor.module
@@ -1,16 +1,18 @@
 <?php
 
+use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Url;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Component\Serialization\Yaml;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\mercury_editor\Entity\MercuryEditorBlockContentForm;
+use Drupal\mercury_editor\Entity\MercuryEditorNodeForm;
+use Drupal\mercury_editor\Entity\MercuryEditorTermForm;
 use Drupal\mercury_editor\EntityTypeInfo;
-use Drupal\Core\Entity\EntityFormInterface;
+use Drupal\Core\Block\BlockPluginInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
-use Laminas\Diactoros\Response\RedirectResponse;
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 
 /**
  * @file
@@ -35,9 +37,6 @@ function mercury_editor_library_info_alter(&$libraries, $extension) {
   if ($extension == 'image_radios' && isset($libraries['image_radios'])) {
     $libraries['image_radios']['dependencies'][] = 'mercury_editor/image_radios';
   }
-  if ($extension == 'claro' && isset($libraries['media_library.theme'])) {
-    $libraries['media_library.theme']['dependencies'][] = 'mercury_editor/claro.media_library.theme';
-  }
 
   if ($extension == 'gin' && isset($libraries['media_library'])) {
     // When using the gin_toolbar module, we need to add the gin_base and
@@ -57,11 +56,8 @@ function mercury_editor_library_info_alter(&$libraries, $extension) {
  * Replaces Drupal's ajax Dialog commands with MercuryDialog commands.
  */
 function mercury_editor_ajax_render_alter(array &$data): void {
-  $current_route_name = \Drupal::routeMatch()->getRouteName();
-  if (
-    isset($current_route_name)
-    && str_starts_with($current_route_name, 'mercury_editor')
-  ) {
+  $route_name = \Drupal::routeMatch()->getRouteName() ?? '';
+  if (str_contains($route_name, 'mercury_editor') || \Drupal::request()->query->has('me_id')) {
     foreach ($data as &$command) {
       if ($command['command'] == 'openDialog') {
         $command['command'] = 'openMercuryDialog';
@@ -70,7 +66,6 @@ function mercury_editor_ajax_render_alter(array &$data): void {
         $command['command'] = 'closeMercuryDialog';
       }
     }
-    Drupal::service('mercury_editor.ajax_adapter')->ajaxRenderAlter($data);
   }
 }
 
@@ -81,17 +76,12 @@ function mercury_editor_ajax_render_alter(array &$data): void {
  * @see contextual_preprocess()
  */
 function mercury_editor_preprocess(array &$variables, $hook, $info) {
-
-  // Set a css class if the entity is being edited with mercury.
-  if (isset($variables['elements']['#is_mercury_edit_mode'])) {
-    $route_match = \Drupal::routeMatch();
-    $route_name = $route_match->getRouteName();
-    if (\Drupal::service('mercury_editor.context')->isPreview() || $route_name == 'mercury_editor.editor') {
-      $variables['page'] = TRUE;
-    }
-    $variables['attributes']['class'][] = 'is-mercury-edit-mode';
+  if (empty($variables['title_suffix']['contextual_links'])) {
+    return;
+  }
+  if (!\Drupal::service('mercury_editor.context')->isPreview()) {
+    return;
   }
-
   // Determine the primary theme function argument.
   if (!empty($info['variables'])) {
     $keys = array_keys($info['variables']);
@@ -107,9 +97,7 @@ function mercury_editor_preprocess(array &$variables, $hook, $info) {
   if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) {
     // Disable contextual links on the preview route.
     $variables['title_suffix']['#cache']['contexts'][] = 'route.name.is_mercury_editor_preview';
-    if (\Drupal::service('mercury_editor.context')->isPreview()) {
-      unset($variables['title_suffix']['contextual_links']);
-    }
+    unset($variables['title_suffix']['contextual_links']);
   }
 }
 
@@ -122,13 +110,17 @@ function mercury_editor_preprocess_layout_paragraphs_builder_controls(&$variable
 
   /** @var \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout */
   $layout = $variables['layout_paragraphs_layout'];
-  $is_mercury_editor_context = $layout->getSetting('mercury_editor_context') ?? FALSE;
   $component = $layout->getComponentByUuid($variables['uuid']);
   $paragraph = $component->getEntity();
   $paragraph_type = $paragraph->bundle();
 
-  if ($is_mercury_editor_context) {
+  if (Drupal::service('mercury_editor.context')->isPreview()) {
 
+    foreach ($variables['controls'] as &$control) {
+      if (isset($control['#url']) && substr($control['#url']->toString(), 0, 1) !== '#') {
+        _mercury_editor_add_me_id($control['#url']);
+      }
+    }
     $variables['controls']['label']['#suffix'] = '<span class="reveal-on-hover">';
     $variables['controls']['delete_link']['#suffix'] = '</span>';
 
@@ -137,20 +129,23 @@ function mercury_editor_preprocess_layout_paragraphs_builder_controls(&$variable
     $component = $layout->getComponentByUuid($uuid);
     $type = $component->getEntity()->getParagraphType();
 
+    // Alters the edit link.
     $edit_dialog_options = json_decode($variables['controls']['edit_link']['#attributes']['data-dialog-options']);
     $edit_dialog_options->height = 'max-content';
     $edit_dialog_options->resizable = TRUE;
     $edit_dialog_options = Drupal::service('mercury_editor.dialog')->dialogSettings(['layout' => $layout, 'dialog' => $paragraph_type . '_form']);
     $variables['controls']['edit_link']['#attributes']['data-dialog-options'] = json_encode($edit_dialog_options);
     $variables['controls']['edit_link']['#attributes']['title'] = t('Edit :type', [':type' => $type->label()]);
-    _mercury_editor_replace_ajax_class($variables['controls']['edit_link']['#attributes']['class']);
     _mercury_editor_replace_layout_paragraphs_routes($variables['controls']['edit_link']['#url']);
 
+    // Alters the delete link.
     $delete_dialog_options = Drupal::service('mercury_editor.dialog')->dialogSettings(['layout' => $layout, 'dialog' => 'delete_form']);
     $variables['controls']['delete_link']['#attributes']['data-dialog-options'] = json_encode($delete_dialog_options);
     $variables['controls']['delete_link']['#attributes']['title'] = t('Delete :type', [':type' => $type->label()]);
-    _mercury_editor_replace_ajax_class($variables['controls']['delete_link']['#attributes']['class']);
     _mercury_editor_replace_layout_paragraphs_routes($variables['controls']['delete_link']['#url']);
+
+    // Alters the duplicate link.
+    _mercury_editor_replace_layout_paragraphs_routes($variables['controls']['duplicate_link']['#url']);
   }
 }
 
@@ -161,31 +156,21 @@ function mercury_editor_preprocess_layout_paragraphs_builder_controls(&$variable
  * of directly invoking an Ajax action.
  */
 function mercury_editor_preprocess_layout_paragraphs_insert_component_btn(&$variables) {
-  /** @var \Drupal\Core\Url $variables['url'] */
-  $parameters = $variables['url']->getRouteParameters();
-  $layout_id = $parameters['layout_paragraphs_layout'];
-  $layout = \Drupal::service('layout_paragraphs.tempstore_repository')->getWithStorageKey($layout_id);
-  $is_mercury_editor_context = $layout->getSetting('mercury_editor_context') ?? FALSE;
-  if (!$is_mercury_editor_context) {
+  if (!Drupal::service('mercury_editor.context')->isPreview()) {
     return;
   }
-  _mercury_editor_replace_ajax_class($variables['attributes']['class']);
+  _mercury_editor_add_me_id($variables['url']);
   _mercury_editor_replace_layout_paragraphs_routes($variables['url']);
   $old_options = json_decode($variables['attributes']['data-dialog-options'], TRUE);
   $dialog_options = ['target' => $old_options['target']] + Drupal::service('mercury_editor.dialog')->dialogSettings(['dialog' => 'component_menu']);
   $variables['attributes']['data-dialog-options'] = json_encode($dialog_options);
 }
 
-/**
- * Helper function to replace use-ajax classes with use-postmessage.
- *
- * @param array $classes
- *   An array of classes.
- */
-function _mercury_editor_replace_ajax_class(&$classes) {
-  if (($key = array_search('use-ajax', $classes)) !== FALSE) {
-    unset($classes[$key]);
-    $classes[] = 'use-postmessage';
+function _mercury_editor_add_me_id(Url &$url) {
+  if ($entity = \Drupal::service('mercury_editor.context')->getEntity()) {
+    $query = $url->getOption('query');
+    $query['me_id'] = $entity->uuid();
+    $url->setOption('query', $query);
   }
 }
 
@@ -233,6 +218,13 @@ function mercury_editor_theme_suggestions_layout_paragraphs_builder_component_me
   if ($route_name === 'mercury_editor.builder.choose_component') {
     $suggestions[] = 'layout_paragraphs_builder_component_menu__mercury_editor';
   }
+  foreach ($variables['types'] as $type => &$links) {
+    foreach ($links as $key => &$link) {
+      if (isset($link['url_object'])) {
+        _mercury_editor_add_me_id($link['url_object']);
+      }
+    }
+  }
 }
 
 /**
@@ -259,25 +251,45 @@ function mercury_editor_entity_build_defaults_alter(array &$build, EntityInterfa
   }
 }
 
+/**
+ * Implements hook_block_build_alter().
+ *
+ * Add cache context for mercury editor preview screen.
+ */
+function mercury_editor_block_build_alter(array &$build, BlockPluginInterface $block) {
+  $me_entity_types = \Drupal::config('mercury_editor.settings')->get('bundles');
+  if (empty($me_entity_types['block_content'])) {
+    return;
+  }
+  $build['#cache']['contexts'][] = 'route.name.is_mercury_editor_preview';
+}
+
 /**
  * Implements hook_build_alter().
  */
 function mercury_editor_entity_display_build_alter(&$build, $context) {
 
-  $route_name = \Drupal::routeMatch()->getRouteName();
-  if (!\Drupal::service('mercury_editor.context')->isPreview() && $route_name !== 'mercury_editor.editor') {
+  if (empty($context['entity'])) {
+    return;
+  }
+  if (!\Drupal::service('mercury_editor.context')->isPreview()) {
+    return;
+  }
+  $mercury_editor_entity = \Drupal::service('mercury_editor.context')->getEntity();
+  if (empty($mercury_editor_entity)) {
     return;
   }
-  /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
-  $entity = $context['entity'];
-  if (!$entity) {
+  if ($context['entity']->uuid() != $mercury_editor_entity->uuid()) {
     return;
   }
-  $build['#is_mercury_edit_mode'] = TRUE;
+
+  // Adds a data attribute to the entity wrapper for Mercury Editor.
+  $build['#attributes']['data-me-edit-screen-key'] = $mercury_editor_entity->uuid();
+  $build['#attributes']['class'][] = 'is-mercury-edit-mode';
 
   // Turns on Layout Paragraphs builder for Mercury Editor LP fields.
-  if (isset($entity->lp_storage_keys)) {
-    foreach ($entity->lp_storage_keys as $field_name => $lp_key) {
+  if (isset($mercury_editor_entity->lp_storage_keys)) {
+    foreach ($mercury_editor_entity->lp_storage_keys as $field_name => $lp_key) {
       if (isset($build[$field_name])) {
         $layout = \Drupal::service('layout_paragraphs.tempstore_repository')->getWithStorageKey($lp_key);
         $build[$field_name] = [
@@ -288,6 +300,7 @@ function mercury_editor_entity_display_build_alter(&$build, $context) {
       }
     }
   }
+
 }
 
 /**
@@ -356,11 +369,16 @@ function mercury_editor_preprocess_html(&$variables) {
         return strpos($value, 'adminimal-admin-toolbar') !== 0;
       });
     }
-    // Check if navigation is enabled.
-    if (\Drupal::moduleHandler()->moduleExists('navigation')) {
-      // Remove navigation from the page.
-      unset($variables['page_top']['navigation']);
-    }
+  }
+}
+
+/**
+ * Implements hook_preprocess_node().
+ */
+function mercury_editor_preprocess_node(&$variables) {
+  $mercury_editor_entity = \Drupal::service('mercury_editor.context')->getEntity();
+  if ($mercury_editor_entity && $variables['node']->uuid() == $mercury_editor_entity->uuid()) {
+    $variables['page'] = TRUE;
   }
 }
 
@@ -407,6 +425,16 @@ function mercury_editor_preprocess_page__mercury_editor(&$variables) {
   }
 }
 
+/**
+ * Implements hook_preprocess_taxonomy_term().
+ */
+function mercury_editor_preprocess_taxonomy_term(&$variables) {
+  $mercury_editor_entity = \Drupal::service('mercury_editor.context')->getEntity();
+  if ($mercury_editor_entity && $variables['term']->uuid() == $mercury_editor_entity->uuid()) {
+    $variables['page'] = TRUE;
+  }
+}
+
 /**
  * Implements hook_theme_registry_alter().
  *
@@ -546,42 +574,63 @@ function mercury_editor_preprocess_layout_paragraphs_builder_component_menu(&$va
   }
 
   $groups = Drupal::config('mercury_editor.menu.settings')->get('groups');
-  $groups_array = !empty($groups)
-    ? Yaml::decode($groups)
-    : [
-        'default' => [
-          'label' => 'Default',
-          'components' => [],
-        ]
+  $types = $variables['types']['content'] + $variables['types']['layout'];
+  $variables['count'] = count($types);
+
+  if (!empty($groups)) {
+    $groups_array = Yaml::decode($groups);
+    $variables['groups'] = [];
+    foreach ($groups_array as $name => &$group) {
+      $variables['groups'][$name] = [
+        'items' => array_filter(array_map(function ($component) use ($types) {
+            return $types[$component] ?? FALSE;
+          }, array_combine($group['components'], $group['components']))),
+        'label' => $group['label'],
       ];
-  $types = $variables['types']['content'];
-  $variables['groups'] = [];
-  foreach ($groups_array as $name => &$group) {
-    $variables['groups'][$name] = [
-      'items' => array_filter(array_map(function ($component) use ($types) {
-          return $types[$component] ?? FALSE;
-        }, array_combine($group['components'], $group['components']))),
-      'label' => $group['label'],
+    }
+    $default_group = key(array_filter($groups_array, function($group) {
+      return !empty($group['default']);
+    })) ?? 'default';
+
+    $variables['groups'] = array_filter($variables['groups'], function($group, $id) use ($default_group) {
+      return count($group['items']) || $id == $default_group;
+    }, ARRAY_FILTER_USE_BOTH);
+
+    $orphaned_types = array_filter(array_keys($types), function($type) use ($variables) {
+      foreach ($variables['groups'] as $group) {
+        if (!empty($group['items'][$type])) {
+          return FALSE;
+        }
+      }
+      return TRUE;
+    });
+    foreach ($orphaned_types as $type) {
+      $variables['groups'][$default_group]['items'][$type] = $types[$type];
+    }
+  }
+  else {
+    $variables['groups'] = [
+      'layout' => [
+        'items' => $variables['types']['layout'],
+        'label' => t('Layout'),
+      ],
+      'content' => [
+        'items' => $variables['types']['content'],
+        'label' => t('Content'),
+      ],
     ];
   }
-  $default_group = key(array_filter($groups_array, function($group) {
-    return !empty($group['default']);
-  })) ?? 'default';
 
-  $variables['groups'] = array_filter($variables['groups'], function($group, $id) use ($default_group) {
-    return count($group['items']) || $id == $default_group;
+  $template_components = array_filter($types, function($group, $id) {
+    return strpos($id, 'me_template_') === 0;
   }, ARRAY_FILTER_USE_BOTH);
-  $orphaned_types = array_filter(array_keys($types), function($type) use ($variables) {
-    foreach ($variables['groups'] as $group) {
-      if (!empty($group['items'][$type])) {
-        return FALSE;
-      }
-    }
-    return TRUE;
-  });
-  foreach ($orphaned_types as $type) {
-    $variables['groups'][$default_group]['items'][$type] = $types[$type];
+  if (!empty($template_components)) {
+    $variables['groups']['templates'] = [
+      'items' => $template_components,
+      'label' => t('Templates'),
+    ];
   }
+
   $variables['#attached']['library'][] = 'mercury_editor/menu';
   $variables['#attached']['library'][] = 'mercury_editor/lpb_component_list';
 }
@@ -591,14 +640,6 @@ function mercury_editor_preprocess_layout_paragraphs_builder_component_menu(&$va
  * Implements hook_preprocess_layout_paragraphs_builder().
  */
 function mercury_editor_preprocess_layout_paragraphs_builder(&$variables) {
-  $entity = $variables['layout_paragraphs_layout']->getEntity();
-  $field_name = $variables['layout_paragraphs_layout']->getFieldName();
-  $field_ref = [
-    $entity->getEntityTypeId(),
-    $entity->id(),
-    $field_name,
-  ];
-  $variables['attributes']['data-lp-field-ref'] = implode('/', $field_ref);
   $variables['#attached']['library'][] = 'mercury_editor/mercury_editor';
 }
 
@@ -633,11 +674,16 @@ function mercury_editor_preprocess_field(&$variables) {
  * Implements hook_entity_type_build().
  */
 function mercury_editor_entity_type_build(array &$entity_types) {
-  if (!empty($entity_types['node'])) {
-    $entity_types['node']->setFormClass('mercury_editor', 'Drupal\mercury_editor\Entity\MercuryEditorNodeForm');
-  }
-  if (!empty($entity_types['taxonomy_term'])) {
-    $entity_types['taxonomy_term']->setFormClass('mercury_editor', 'Drupal\mercury_editor\Entity\MercuryEditorTermForm');
+  $entity_forms = [
+    'node' => MercuryEditorNodeForm::class,
+    'taxonomy_term' => MercuryEditorTermForm::class,
+    'block_content' => MercuryEditorBlockContentForm::class,
+  ];
+
+  foreach ($entity_forms as $entity_type => $form_class) {
+    if (isset($entity_types[$entity_type]) && $entity_types[$entity_type] instanceof EntityTypeInterface) {
+      $entity_types[$entity_type]->setFormClass('mercury_editor', $form_class);
+    }
   }
 }
 
@@ -689,40 +735,6 @@ function mercury_editor_module_implements_alter(&$implementations, $hook) {
   }
 }
 
-/**
- * Implements hook_preprocess_toolbar().
- *
- * Replaces the edit link with the Mercury Editor edit link.
- */
-function mercury_editor_preprocess_toolbar(&$variables) {
-  if (!empty($variables['entity_edit_url'])) {
-    $entity = $variables['entity_edit_url']->getOption('entity');
-    $content_type = $entity->bundle();
-    if (_mercury_editor_applies_to_content_type($content_type)) {
-      $options = $variables['entity_edit_url']->getOptions();
-      $parameters = $variables['entity_edit_url']->getRouteParameters();
-      $parameters['mercury_editor_entity'] = $entity->uuid();
-      $parameters['entity_type'] = $entity->getEntityTypeId();
-      $variables['entity_edit_url'] = Url::fromRoute('mercury_editor.editor', $parameters, $options);
-    }
-  }
-}
-
-/**
- * Implements hook_entity_view().
- */
-function mercury_editor_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
-  // @todo Need a better way to define when this gets added.
-  $me_entity_types = \Drupal::config('mercury_editor.settings')->get('bundles');
-  if ($view_mode == 'full' && isset($me_entity_types[$entity->getEntityTypeId()])) {
-    $build['#attributes']['data-me-edit-screen-key'] = $entity->uuid();
-    $id = $entity->id();
-    if ($id) {
-      $build['#attributes']['data-me-edit-screen-entity-id'] = $entity->id();
-    }
-  }
-}
-
 /**
  * Impelements hook_preprocess_layout_paragraphs_builder_formatter().
  */
@@ -731,13 +743,6 @@ function mercury_editor_preprocess_layout_paragraphs_builder_formatter(&$variabl
   $variables['#attached']['library'][] = 'mercury_editor/field_formatter';
 }
 
-/**
- * Implements hook_entity_view_alter().
- */
-function mercury_editor_entity_view_alter(&$build, $entity, $display) {
-  $build['#attached']['library'][] = 'mercury_editor/me_dialog';
-}
-
 /**
  * Implements hook_form_FORM_ID_alter().
  * Implements hook_form_layout_paragraphs_builder_form_alter().
@@ -863,6 +868,9 @@ function mercury_editor_after_build_radios($element, $form_state) {
   return $element;
 }
 
+/**
+ * After callback for attaching the horizontal tabs library.
+ */
 function mercury_editor_after_build_form($form, $form_state) {
   if (isset($form['tabs']) && count($form['tabs']['#options']) < 2) {
     $form['tabs']['#access'] = FALSE;
@@ -873,7 +881,6 @@ function mercury_editor_after_build_form($form, $form_state) {
   return $form;
 }
 
-
 /**
  * Implements hook_form_FORM_ID_alter().
  * Implements hook_layout_paragraphs_delete_component_form_alter().
@@ -882,20 +889,3 @@ function mercury_editor_form_layout_paragraphs_delete_component_form_alter(&$for
   $form['actions']['#attributes']['class'][] = 'me-form-actions';
   $form['#attached']['library'][] = 'mercury_editor/lpb_component_delete_form';
 }
-
-/**
- * Returns TRUE if $type uses Mercury Editor.
- *
- * @param string $type
- *   The content type.
- *
- * @return bool
- *   TRUE if applies to content type.
- */
-function _mercury_editor_applies_to_content_type($type) {
-  $content_types = \Drupal::configFactory()->get('mercury_editor.settings')->get('content_types');
-  if (empty($content_types[$type])) {
-    return FALSE;
-  }
-  return TRUE;
-}
diff --git a/mercury_editor.routing.yml b/mercury_editor.routing.yml
index 2e7310039f34be6bf6257d1a4c463f8898540898..22468492cab9c0ee67a3ae187a671682c3491ac4 100644
--- a/mercury_editor.routing.yml
+++ b/mercury_editor.routing.yml
@@ -45,7 +45,7 @@ mercury_editor.dialog_settings:
 
 # Mercury Editor replacements for Layout Paragraphs Builder routes.
 mercury_editor.builder.insert:
-  path: '/mercury-editor/{layout_paragraphs_layout}/insert/{paragraph_type}'
+  path: '/mercury-editor/{layout_paragraphs_layout}/insert/{paragraph_type_id}'
   defaults:
     _controller: '\Drupal\mercury_editor\Controller\InsertComponentController::skipInsertForm'
     operation: 'create'
@@ -53,8 +53,6 @@ mercury_editor.builder.insert:
     parameters:
       layout_paragraphs_layout:
         layout_paragraphs_layout_tempstore: TRUE
-      paragraph_type:
-        type: entity:paragraphs_type
   requirements:
     _layout_paragraphs_builder_access: 'TRUE'
 mercury_editor.builder.edit_item:
@@ -68,6 +66,17 @@ mercury_editor.builder.edit_item:
         layout_paragraphs_layout_tempstore: TRUE
   requirements:
     _layout_paragraphs_builder_access: 'TRUE'
+mercury_editor.builder.duplicate_item:
+  path: '/mercury-editor/{layout_paragraphs_layout}/duplicate/{source_uuid}'
+  defaults:
+    _controller: '\Drupal\mercury_editor\Controller\DuplicateController::duplicate'
+    operation: 'duplicate'
+  options:
+    parameters:
+      layout_paragraphs_layout:
+        layout_paragraphs_layout_tempstore: TRUE
+  requirements:
+    _layout_paragraphs_builder_access: 'TRUE'
 mercury_editor.builder.delete_item:
   path: '/mercury-editor/{layout_paragraphs_layout}/delete/{component_uuid}'
   defaults:
diff --git a/mercury_editor.services.yml b/mercury_editor.services.yml
index b27f04b77a586f03ef7ce5b9281c97d3e77a846e..60fcea43d0ebd3fb496d093bf6ea3b1d0eba6c56 100644
--- a/mercury_editor.services.yml
+++ b/mercury_editor.services.yml
@@ -11,12 +11,6 @@ services:
     arguments: ['@config.factory', '@request_stack']
     tags:
       - { name: theme_negotiator, priority: 1500 }
-  mercury_editor.ajax_adapter:
-    class: Drupal\mercury_editor\AjaxResponseIframeAdapter
-    arguments:
-      - '@theme.manager'
-      - '@config.factory'
-      - '@theme.initialization'
   mercury_editor.iframe_ajax_response_wrapper:
     class: Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper
     arguments:
@@ -24,6 +18,7 @@ services:
       - '@config.factory'
       - '@theme.initialization'
       - '@mercury_editor.attachments_processor'
+      - '@mercury_editor.context'
   mercury_editor.attachments_processor:
     class: Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapperAttachmentsProcessor
     parent: ajax_response.attachments_processor
@@ -32,7 +27,7 @@ services:
     arguments: ['@config.factory']
   cache_context.route.name.is_mercury_editor_preview:
     class: Drupal\mercury_editor\Cache\MercuryEditorPreviewCacheContext
-    arguments: ['@current_route_match']
+    arguments: ['@current_route_match', '@mercury_editor.context']
     tags:
       - { name: cache.context }
   mercury_editor.param_converter:
@@ -61,7 +56,7 @@ services:
       - { name: event_subscriber }
   mercury_editor.context:
     class: Drupal\mercury_editor\MercuryEditorContextService
-    arguments: ['@current_route_match']
+    arguments: ['@current_route_match', '@layout_paragraphs.tempstore_repository', '@mercury_editor.tempstore_repository', '@request_stack']
   mercury_editor.preview_routes_subscriber:
     class: \Drupal\mercury_editor\Routing\MercuryEditorPreviewRoutes
     arguments: ['@entity_type.manager']
diff --git a/modules/mercury_editor_templates/mercury_editor_templates.module b/modules/mercury_editor_templates/mercury_editor_templates.module
index a616f8f46ae1ae48ff196d88d9714886aa033990..23aa9b28eaa427e54aca448710b20c98008eff5c 100644
--- a/modules/mercury_editor_templates/mercury_editor_templates.module
+++ b/modules/mercury_editor_templates/mercury_editor_templates.module
@@ -100,7 +100,7 @@ function mercury_editor_templates_preprocess_layout_paragraphs_builder_controls(
       '#attributes' => [
         'class' => [
           'lpb-save-as-template',
-          'use-postmessage',
+          'use-ajax',
         ],
         'data-dialog-type' => 'dialog',
         'data-dialog-options' => Json::encode($dialog_settings),
diff --git a/source/css/components/drag-hint.css b/source/css/components/drag-hint.css
index 65400868988160b2de3775921b66aba37a0e000f..1e98353fafbe014122a0211bfe227a23249a1768 100644
--- a/source/css/components/drag-hint.css
+++ b/source/css/components/drag-hint.css
@@ -1,3 +1,15 @@
 .lp-hint {
+  z-index: 1000;
+}
+.lp-hint[data-orientation="vertical"] {
+  top: 0;
+  border-right: 5px solid blue;
+}
+.lp-hint[data-orientation="horizontal"] {
+  left: 0;
+  width: 100%;
   border-top: 5px solid blue;
 }
+.gu-mirror {
+  opacity: 1 !important;
+}
diff --git a/source/css/components/form.css b/source/css/components/form.css
index 90cc189cfc48cf653ba4775e665ef1d7274bc108..fb236a96595dbd8d3cbc07e274941fecb13952bc 100644
--- a/source/css/components/form.css
+++ b/source/css/components/form.css
@@ -64,7 +64,7 @@ mercury-dialog .form-item__description {
     }
   }
 
-  & input:not([type="checkbox"]):not([type="radio"]),
+  & input:not([type="checkbox"]):not([type="radio"]):not(.media-library-item__remove),
   & textarea,
   & select {
     height: auto;
diff --git a/source/css/components/frontend-builder.css b/source/css/components/frontend-builder.css
index 07af716f27ff9006d15cadf633c472755d9cb83d..18fc08d13404ab56306c8c1f3db95a5624ba48b0 100644
--- a/source/css/components/frontend-builder.css
+++ b/source/css/components/frontend-builder.css
@@ -28,13 +28,14 @@
   * Component hover styles. Uses a class instead of hover state for slight
   * pause, to avoid jumpiness.
   */
-.js-lpb-component.focused {
+.lp-builder:not(.is-navigating) .js-lpb-component.focused {
   outline: 1px solid blue;
+  z-index: 1000;
 }
-.js-lpb-component.focused .js-lpb-region {
+.lp-builder:not(.is-navigating) .js-lpb-component.focused .js-lpb-region {
   outline: 1px dotted rgba(0, 0, 255, 0.5);
 }
-.js-lpb-component.focused > .js-lpb-ui {
+.lp-builder:not(.is-navigating) .js-lpb-component.focused > .js-lpb-ui {
   opacity: 1;
 }
 
@@ -42,6 +43,9 @@
  * Mercury Editor controls modifications.
  */
 .is-mercury-edit-mode {
+  & .lp-builder {
+    z-index: 100;
+  }
   & .lpb-controls {
     padding: 0 5px 0 0;
     border-radius: 4px;
@@ -74,6 +78,25 @@
     font-size: .7em;
     letter-spacing: 2px;
   }
+  & .lpb-tooltiptext {
+    left: var(--me-lpb-tooltip-text-left, -12px);
+    overflow: hidden;
+    clip: rect(1px, 1px, 1px, 1px);
+    width: 1px;
+    height: 1px;
+    word-wrap: normal;
+  }
+  & .lp-builder:not(.is-dragging) .lpb-tooltip--focus:focus + .lpb-tooltiptext,
+  & .lp-builder:not(.is-dragging) .lpb-tooltip--hover:hover + .lpb-tooltiptext,
+  & .lpb-tooltiptext--visible {
+    overflow: visible;
+    clip: auto;
+    width: auto;
+    height: auto;
+  }
+  & .lpb-tooltiptext::after {
+    left: var(--me-lpb-tooltip-text-arrow-left, 20px);
+  }
 }
 
 @keyframes controlsSlideOpen {
diff --git a/source/css/menu.css b/source/css/menu.css
index 25fed24c47079cc8c748f11434cb6c723da3ea5d..1e2641e40e57beb9ea4a837b366d78c2a89b4c41 100644
--- a/source/css/menu.css
+++ b/source/css/menu.css
@@ -89,4 +89,7 @@
   &.type-card a::before {
     background: url('../../images/menu-icons/icon-card.png');
   }
+  &.type-tabs a::before {
+    background: url('../../images/menu-icons/icon-tabs.png');
+  }
 }
diff --git a/source/js/component-form.js b/source/js/component-form.js
index 69df0b01f9e9ff51b811a2d4fc11273d35f86a5f..8fee3d697d5e40e4fd62eb222d672278dd3606cc 100644
--- a/source/js/component-form.js
+++ b/source/js/component-form.js
@@ -10,7 +10,7 @@
           layoutSelect.focus();
         }
       }
-      const form = once('me-component-form', '.layout-paragraphs-component-form')[0];
+      const form = once('me-component-form', 'mercury-dialog .layout-paragraphs-component-form')[0];
       if (form) {
         form.closest('mercury-dialog').addEventListener('open', (e) => {
           const dialog = e.target.shadowRoot.querySelector('dialog');
diff --git a/source/js/edit-screen.js b/source/js/edit-screen.js
index 6d8403a748b719c10267e54da4eef9c2e3e0e092..259b66661d3cbed996744a7afd633a5a6bc2d622 100644
--- a/source/js/edit-screen.js
+++ b/source/js/edit-screen.js
@@ -142,6 +142,7 @@
         sidebarToggle.classList.remove('me-button--sidebar-expand');
         sidebarToggle.classList.add('me-button--sidebar-collapse');
         sidebarToggle.innerHTML = `<span>${Drupal.t('Hide sidebar')}</span>`;
+        sidebarToggle.setAttribute('title', Drupal.t('Hide sidebar'));
         localStorage.removeItem('mercury-dialog-dock-collapsed');
       }
       else {
@@ -149,6 +150,7 @@
         sidebarToggle.classList.remove('me-button--sidebar-collapse');
         sidebarToggle.classList.add('me-button--sidebar-expand');
         sidebarToggle.innerHTML = `<span>${Drupal.t('Show sidebar')}</span>`;
+        sidebarToggle.setAttribute('title', Drupal.t('Show sidebar'));
         localStorage.setItem('mercury-dialog-dock-collapsed', 'true');
       }
 
@@ -217,6 +219,11 @@
           document.addEventListener('mouseup', iFramePointerEventsToggle);
         }
       }
+      // Set the iframe URL once other js files have loaded.
+      if (once('me-preview-iframe', '#me-preview', context).length) {
+         const iframe = document.querySelector('#me-preview');
+         iframe.src = iframe.getAttribute('data-src');
+      }
     }
   }
 })(Drupal, drupalSettings, jQuery, once)
diff --git a/source/js/horizontal-tabs.js b/source/js/horizontal-tabs.js
index e03030056d887e9fc8fd0c40aca0b253cdbf695e..db5f8c48f5c065cd81eeeece5e82a01fa0ad872c 100644
--- a/source/js/horizontal-tabs.js
+++ b/source/js/horizontal-tabs.js
@@ -21,6 +21,17 @@
         const selected = tabs.querySelector('input[type="radio"]:checked') || {};
         toggleTabs(selected.value);
       });
+      if (document.querySelector('.me-tab-group .error')) {
+        const tabGroup = document.querySelector('.me-tab-group .error').closest('.me-tab-group');
+        // Get the class that starts with "me-tab-group--" and get the part after "--".
+        const selectedValue = tabGroup.className.match(/me-tab-group--([^ ]+)/)[1];
+        // Get the tab radio button with the same value as the tab group.
+        const selected = document.querySelector(`.me-tabs input[type="radio"][value="${selectedValue}"]`);
+        if (selected) {
+          selected.checked = true;
+          toggleTabs(selectedValue);
+        }
+      }
     }
   }
 
diff --git a/source/js/me-dragula.js b/source/js/me-dragula.js
deleted file mode 100644
index 818ac68ec73b0cba8de73fa7ad17fc6cd4de9741..0000000000000000000000000000000000000000
--- a/source/js/me-dragula.js
+++ /dev/null
@@ -1,200 +0,0 @@
-(($, Drupal, once) => {
-
-  /**
-   * Ensures that all layout paragraphs controls are fully within viewport.
-   *
-   * @param {jQuery} $builder
-   *   The Layout Paragraphs container jQuery object.
-   */
-  function repositionControls($builder) {
-    $builder.find('.lpb-controls').each((i, controls) => {
-      controls.setAttribute('style', controls.getAttribute('data-style'));
-      const bounding = controls.getBoundingClientRect();
-      // Left viewport edge.
-      const l = 0;
-      // Right viewport edge.
-      const r = (window.innerWidth || document.documentElement.clientWidth);
-      // Overlapping left.
-      if (bounding.left < l) {
-        controls.setAttribute('data-style', controls.getAttribute('style'));
-        $(controls).offset({left: 0});
-      }
-      // Overlapping right.
-      if (bounding.right > r) {
-        $(controls).css({right: (bounding.right - r) + 'px'});
-      }
-    });
-  }
-
-  /**
-   * Simplifies drag and drop visual cues to prevent jumpiness.
-   *
-   * The default behavior of the dragula library can create excessive
-   * jumpiness in some cases. This function simplifies the UI and drag and drop
-   * experience in several key ways, including:
-   *
-   * - Detaches all layout paragraphs UI elements when dragging starts.
-   * - Provides a simple "hint" element to show where an item will be dropped.
-   * - Leaves a "ghost" copy of the grabbed element in place at the source.
-   * - Reattaches all UI elements when dragging ends.
-   *
-   * @see https://github.com/bevacqua/dragula#drakeon-events.
-   *
-   * @param {Object} drake
-   *   The dragula object.
-   */
-  function simplifyDragHints($builder, settings) {
-
-    const drake = $builder.data('drake');
-    let ghost, grabbed;
-    const hint = $('<div class="lp-hint hidden"></div>')[0];
-
-    // Hide UI elements when dragging starts.
-    drake.on('drag', (el) => {
-      el.parentNode.insertBefore(hint, el);
-      $builder.find('.js-lpb-ui').addClass('hidden');
-    });
-    // Provide a simple hint element to indicate where an item will be dropped.
-    drake.on('shadow', (item, container) => {
-      if (item.classList.contains('lp-hint')) {
-        return;
-      }
-      // Remove comments and text nodes from container.
-      [...container.childNodes].filter((e) => e.classList === undefined).forEach((e) => e.remove());
-      container.replaceChild(hint, item);
-
-      // Ensure the hint does not get at the end of the region after the add button.
-      if (hint.nextSibling === null && hint.previousSibling !== null && hint.previousSibling.classList.contains('lpb-btn--add')) {
-        container.insertBefore(hint, hint.previousSibling);
-      }
-
-      const nextIsGhost = hint.nextSibling !== null ? hint.nextSibling.classList.contains('lp-ghost') : false;
-      const prevIsGhost = hint.previousSibling !== null ? hint.previousSibling.classList.contains('lp-ghost') : false;
-      const ghostAdjacent = nextIsGhost || prevIsGhost;
-      if (ghostAdjacent) {
-        hint.classList.add('hidden');
-        ghost.classList.remove('gu-transit');
-      }
-      else {
-        hint.classList.remove('hidden');
-        ghost.classList.add('gu-transit');
-      }
-    });
-    // Leave a copy of the grabbed item in place at the original source.
-    drake.on('cloned', (mirror, item) => {
-      ghost = item.cloneNode(true);
-      ghost.classList.add('lp-ghost');
-      item.parentNode.insertBefore(ghost, item);
-      grabbed = item;
-      item.remove();
-    });
-    // Show UI elements and remove ghost and hint elements when dragging stops.
-    drake.on('dragend', (el) => {
-      hint.replaceWith(grabbed);
-      ghost.remove();
-      $builder.find('.js-lpb-ui').removeClass('hidden');
-      repositionControls($builder);
-      $builder.trigger('lpb-component:drop', [el.getAttribute('data-uuid')]);
-    });
-  }
-
-  /**
-   * Waits for a condition to be met, then calls the provided callback.
-   *
-   * @param {Function} cond
-   *   The condition to wait for.
-   * @param {Function} cb
-   *   The callback to call when cond evaluates true.
-   */
-  function waitFor(cond, cb) {
-    const i = setInterval(() => {
-      if (cond() === true) {
-        clearInterval(i);
-        cb();
-      }
-    }, 100);
-  }
-
-  function removeMovedFormActions() {
-    const outdatedFormActions = document.getElementsByClassName('lpb-form__actions repositioned');
-    if (outdatedFormActions.length) {
-      outdatedFormActions[0].remove();
-    }
-  }
-
-  function moveFormActions(context) {
-    const formActions = context.querySelector('.lpb-form__actions');
-    if (formActions) {
-      removeMovedFormActions();
-      formActions.classList.add('repositioned');
-      document.body.appendChild(formActions);
-    }
-  }
-
-  function showControls(e) {
-    const el = e.target.closest('.lpb-controls, .js-lpb-component');
-    el.classList.add('focused');
-    el.classList.remove('transitioning');
-    el.classList.remove('blurred');
-  }
-
-  function hideControls(e) {
-    const el = e.target.closest('.lpb-controls, .js-lpb-component');
-    el.classList.add('transitioning');
-    setTimeout(() => {
-      if (el.classList.contains('transitioning')) {
-        el.classList.remove('focused');
-        el.classList.remove('transitioning');
-        el.classList.add('blurred');
-      }
-    }, 250);
-  }
-
-  Drupal.behaviors.mercuryEditorDragula = {
-    attach: function attach(context, settings) {
-      // Append form-actions to body for better styling control.
-      if (once('me-builder-events', 'html', context).length) {
-        $(window).on('lpb-builder:open.lpb lpb-builder:save.lpb', (e) => {
-          moveFormActions(context);
-        });
-        $(window).on('lpb-builder:close.lpb', (e) => {
-          removeMovedFormActions();
-        });
-      }
-      var events = ['lpb-component:insert.lpb', 'lpb-component:update.lpb', 'lpb-component:move.lpb', 'lpb-component:drop.lpb'].join(' ');
-      $(once('me-builder-form', '[data-lpb-id]', context)).on(events, function (e) {
-        const cancelButton = document.querySelector('lpb-form__actions repositioned .lpb-btn--cancel');
-        if (cancelButton) {
-          cancelButton.value = Drupal.t('Cancel')
-        }
-      });
-      once('me-dragula', '.lp-builder.has-components').forEach((builder) => {
-        const $builder = $(builder);
-        waitFor(
-          () => $builder.data('drake') !== undefined,
-          () => {
-            repositionControls($builder);
-            moveFormActions($builder[0]);
-            $(window).resize(() => repositionControls($builder));
-            // Simplifies drag and drop functionality.
-            try {
-              simplifyDragHints($builder, settings);
-            }
-            catch (e) {
-              console.warn(e);
-            }
-          });
-      });
-      once('reveal-on-hover', '.js-lpb-component').forEach((component) => {
-        component.addEventListener('mouseenter', showControls);
-        component.addEventListener('mouseleave', hideControls);
-      });
-      once('reveal-on-hover', '.lpb-controls').forEach((el) => {
-        el.addEventListener('mouseenter', showControls);
-        el.addEventListener("focusin", showControls);
-        el.addEventListener('mouseleave', hideControls);
-        el.addEventListener("focusout", hideControls);
-      });
-    }
-  }
-})(jQuery, Drupal, once);
diff --git a/source/js/post-messages-listener.js b/source/js/post-messages-listener.js
index 5df43c773d28f05d0a2fec21d2ecc593ce7496c5..6cd3b913ed118f22b57dc35a00ddfb916f44243b 100644
--- a/source/js/post-messages-listener.js
+++ b/source/js/post-messages-listener.js
@@ -8,14 +8,6 @@
      * @param {Object} settings The ajax settings.
      */
     drupalAjax: function (settings) {
-      console.log('drupalAjax', settings);
-      Drupal.ajax(settings).execute();
-    },
-    /**
-     * Ajax click handler for Layout Paragraphs UI elements in iframe.
-     * @param {Object} settings The ajax settings.
-     */
-    lpbUiClick: function (settings) {
       Drupal.ajax(settings).execute();
     },
     /**
diff --git a/source/js/preview-screen.js b/source/js/preview-screen.js
index 33e6bc524cb6e97a9375d517c520f1161d264242..509423b0efb9d7bdabd71685435432ab30c5f1de 100644
--- a/source/js/preview-screen.js
+++ b/source/js/preview-screen.js
@@ -1,4 +1,4 @@
-((Drupal, drupalSettings, once) => {
+((Drupal, drupalSettings, $, once) => {
   'use strict';
 
   /**
@@ -30,7 +30,7 @@
     e.currentTarget.classList.add('is-me-focused');
     // Then, send the click event.
     const message = {
-      type: 'lpbUiClick',
+      type: 'drupalAjax',
       settings: {
         dialogType: e.currentTarget.getAttribute('data-dialog-type'),
         dialog: JSON.parse(e.currentTarget.getAttribute('data-dialog-options')),
@@ -44,18 +44,202 @@
     return false;
   }
 
+  function scaleMirror(e) {
+    const mirror = document.querySelector('.gu-mirror');
+    if (!mirror) {
+      return;
+    }
+    const scaleAxis = mirror.offsetWidth > mirror.offsetHeight ? 'offsetHeight' : 'offsetWidth';
+    const scale = Math.min(300 / mirror[scaleAxis], 1);
+    const boundingRect = mirror.getBoundingClientRect();
+    const diffX = e.clientX - boundingRect.x;
+    const diffY = e.clientY - boundingRect.y;
+    mirror.style.setProperty('transform-origin', `${diffX}px ${diffY}px`);
+    mirror.style.setProperty('transform', `scale(${scale})`);
+    window.removeEventListener('mousemove', scaleMirror);
+  }
+
+  /**
+   * Simplifies drag and drop visual cues to prevent jumpiness.
+   *
+   * The default behavior of the dragula library can create excessive
+   * jumpiness in some cases. This function simplifies the UI and drag and drop
+   * experience in several key ways, including:
+   *
+   * - Detaches all layout paragraphs UI elements when dragging starts.
+   * - Provides a simple "hint" element to show where an item will be dropped.
+   * - Leaves a "ghost" copy of the grabbed element in place at the source.
+   * - Reattaches all UI elements when dragging ends.
+   *
+   * @see https://github.com/bevacqua/dragula#drakeon-events.
+   *
+   * @param {Object} drake
+   *   The dragula object.
+   */
+  function simplifyDragHints($builder, drake) {
+
+    let ghost, grabbed;
+    const hint = $('<div class="lp-hint hidden"></div>')[0];
+    // Scales the mirror element to make it easier to drag.
+    drake.on('cloned', (clone, original, type) => {
+      window.addEventListener('mousemove', scaleMirror);
+    });
+    // Hide UI elements when dragging starts.
+    drake.on('drag', (el) => {
+      if (el.parentNode) {
+        el.parentNode.insertBefore(hint, el);
+      }
+      $builder.find('.js-lpb-ui').addClass('hidden');
+    });
+    // Provide a simple hint element to indicate where an item will be dropped.
+    drake.on('shadow', (shadow, container, src) => {
+
+      if (shadow.classList.contains('lp-hint')) {
+        return;
+      }
+
+      hint.style = {
+        width: '',
+        height: '',
+        marginLeft: '',
+        marginTop: '',
+      }
+
+      const sibling = shadow.nextElementSibling || shadow.previousElementSibling;
+      const orientation = sibling && shadow.getBoundingClientRect().top === sibling.getBoundingClientRect().top
+          ? 'vertical'
+          : 'horizontal';
+
+      if (orientation == 'horizontal') {
+        const offset = parseInt(window.getComputedStyle(shadow.parentNode).getPropertyValue('padding-left'));
+        hint.style.marginLeft = '-' + offset + 'px';
+      }
+
+      if (orientation == 'vertical') {
+        const offset = parseInt(window.getComputedStyle(shadow.parentNode).getPropertyValue('padding-top'));
+        hint.style.marginTop = '-' + offset + 'px';
+      }
+
+      if (orientation === 'vertical') {
+        hint.style.height = shadow.parentNode.clientHeight + 'px';
+      }
+      if (orientation === 'horizontal') {
+        hint.style.width = shadow.parentNode.clientWidth + 'px';
+      }
+
+      hint.setAttribute('data-orientation', orientation);
+
+      // Remove comments and text nodes from container.
+      [...container.childNodes].filter((e) => e.classList === undefined).forEach((e) => e.remove());
+      container.replaceChild(hint, shadow);
+
+      // Ensure the hint does not get at the end of the region after the add button.
+      if (hint.nextSibling === null && hint.previousSibling !== null && hint.previousSibling.classList.contains('lpb-btn--add')) {
+        container.insertBefore(hint, hint.previousSibling);
+      }
+
+      const nextIsGhost = hint.nextSibling !== null ? hint.nextSibling.classList.contains('lp-ghost') : false;
+      const prevIsGhost = hint.previousSibling !== null ? hint.previousSibling.classList.contains('lp-ghost') : false;
+      const ghostAdjacent = nextIsGhost || prevIsGhost;
+      if (ghostAdjacent) {
+        hint.classList.add('hidden');
+        ghost.classList.remove('gu-transit');
+      }
+      else {
+        hint.classList.remove('hidden');
+        ghost.classList.add('gu-transit');
+      }
+    });
+    // Leave a copy of the grabbed item in place at the original source.
+    drake.on('cloned', (mirror, item) => {
+      ghost = item.cloneNode(true);
+      ghost.classList.add('lp-ghost');
+      if (item.parentNode) {
+        item.parentNode.insertBefore(ghost, item);
+      }
+      grabbed = item;
+      item.remove();
+    });
+    // Show UI elements and remove ghost and hint elements when dragging stops.
+    drake.on('dragend', (el) => {
+      hint.replaceWith(grabbed);
+      ghost.remove();
+      $builder.find('.js-lpb-ui').removeClass('hidden');
+    });
+  }
+
+  /**
+   * Calls simplifyDragHints() when the builder is initialized.
+   */
+  $(document).on('lpb-builder:init', (e) => {
+    const builder = e.target;
+    const drake = $(builder).data('drake');
+    if (drake) {
+      simplifyDragHints($(builder), drake);
+    }
+  });
+
+  function padElements(layout) {
+    [...layout.querySelectorAll('[data-region]'), layout].forEach((el) => {
+      const computed = getComputedStyle(el);
+      if (!el.hasAttribute('data-me-padding')) {
+        el.setAttribute('data-me-padding', `${computed.paddingTop} ${computed.paddingRight} ${computed.paddingBottom} ${computed.paddingLeft}`);
+      }
+      el.style.paddingTop = Math.max(10, parseInt(computed.paddingTop)) + 'px';
+      el.style.paddingRight = Math.max(10, parseInt(computed.paddingRight)) + 'px';
+      el.style.paddingBottom = Math.max(10, parseInt(computed.paddingBottom)) + 'px';
+      el.style.paddingLeft = Math.max(10, parseInt(computed.paddingLeft)) + 'px';
+    });
+  }
+
+  function unpadElements(layout) {
+    [...layout.querySelectorAll('[data-region]'), layout].forEach((el) => {
+      if (el.hasAttribute('data-me-padding')) {
+        const computed = getComputedStyle(el);
+        el.style.padding = el.getAttribute('data-me-padding');
+      }
+    });
+  }
+
+  function showControls(e) {
+    const el = e.target.closest('.lpb-controls, .js-lpb-component');
+    el.classList.add('focused');
+    el.classList.remove('transitioning');
+    el.classList.remove('blurred');
+  }
+
+  function hideControls(e) {
+    const el = e.target.closest('.lpb-controls, .js-lpb-component');
+    el.classList.add('transitioning');
+    setTimeout(() => {
+      if (el.classList.contains('transitioning')) {
+        el.classList.remove('focused');
+        el.classList.remove('transitioning');
+        el.classList.add('blurred');
+      }
+    }, 250);
+  }
+
   /**
    * Attaches the behavior to the edit screen.
    */
   Drupal.behaviors.mercuryEditorPreviewScreen = {
     attach: function(context, _settings) {
+      const duplicateContainers = [...document.querySelectorAll('[data-me-edit-screen-key]')]
+        .map((container) => container.getAttribute('data-me-edit-screen-key'))
+        // check for duplicates in array
+        .filter((value, index, self) => self.indexOf(value) !== index);
+      if (duplicateContainers.length > 0) {
+        console.error('Multiple HTML elements found using the same data attribute, "data-me-edit-screen-key", which should be unique. Make sure attributes are not passed to child elements in twig templates.', duplicateContainers);
+      }
       // Send the initial ajaxPageState to the parent window.
       window.parent.postMessage({
         type: 'ajaxPreviewPageState',
         settings: drupalSettings.ajaxPageState
       });
       // Attaches click handlers to links that use window.postMessage().
-      once('me-msg-broadcaster', '.use-postmessage').forEach((el) => {
+      once('me-msg-broadcaster', '.js-lpb-ui.use-ajax, .js-lpb-ui .use-ajax').forEach((el) => {
+        $(el).off();
         el.addEventListener('mousedown', preventDefault);
         el.addEventListener('mouseup', preventDefault);
         el.addEventListener('click', lpbClickHander);
@@ -71,16 +255,50 @@
               return false;
             });
           }
+          else {
+            // The dragula library prevents links from automatically focusing
+            // on mousedown, which can cause issues with keyboard navigation.
+            link.addEventListener('mousedown', (e) => e.target.focus());
+          }
         });
         once('me-prevent-focus', 'a, button, input, textarea, select, details', context).forEach((focussable) => {
           if (
             focussable.closest('.lpb-controls') === null &&
+            focussable.closest('.mercury-editor-ui') === null &&
             !focussable.classList.contains('use-postmessage')
           ) {
             focussable.setAttribute('tabindex', '-1');
           }
         });
+        once('me-layout-hover', '.lpb-layout').forEach((layout) => {
+          layout.addEventListener('mouseenter', (e) => {
+            e.target.setAttribute('data-mouseover', 'true');
+            setTimeout(() => {
+              if (e.target.getAttribute('data-mouseover')) {
+                padElements(e.target);
+              }
+            }, 100);
+          });
+          layout.addEventListener('mouseleave', (e) => {
+            e.target.removeAttribute('data-mouseover');
+            setTimeout(() => {
+              if (!e.target.getAttribute('data-mouseover')) {
+                unpadElements(e.target);
+              }
+            }, 100);
+          });
+        });
       }
+      once('reveal-on-hover', '.js-lpb-component').forEach((component) => {
+        component.addEventListener('mouseenter', showControls);
+        component.addEventListener('mouseleave', hideControls);
+      });
+      once('reveal-on-hover', '.lpb-controls').forEach((el) => {
+        el.addEventListener('mouseenter', showControls);
+        el.addEventListener("focusin", showControls);
+        el.addEventListener('mouseleave', hideControls);
+        el.addEventListener("focusout", hideControls);
+      });
     }
   }
-})(Drupal, drupalSettings, once)
+})(Drupal, drupalSettings, jQuery, once)
diff --git a/src/Ajax/IFrameAjaxResponseWrapper.php b/src/Ajax/IFrameAjaxResponseWrapper.php
index 5d558b5af57ef2c3cdd05ecb0ad3fce2909aaf85..e313531ae3914970c2ff1eafe4b97ae9cc145755 100644
--- a/src/Ajax/IFrameAjaxResponseWrapper.php
+++ b/src/Ajax/IFrameAjaxResponseWrapper.php
@@ -8,6 +8,7 @@ use Drupal\Core\Theme\ThemeManagerInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Theme\ThemeInitializationInterface;
 use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
+use Drupal\mercury_editor\MercuryEditorContextService;
 
 /**
  * Ajax response wrapper for IFrame.
@@ -53,8 +54,15 @@ class IFrameAjaxResponseWrapper extends AjaxResponse {
    *   The theme initializer.
    * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $ajax_response_attachments_processor
    *   The ajax response attachments processor.
+   * @param \Drupal\mercury_editor\MercuryEditorContextService $mercuryEditorContext
+   *   The mercury editor context service.
    */
-  public function __construct(ThemeManagerInterface $theme_manager, ConfigFactoryInterface $config_factory, ThemeInitializationInterface $theme_initializer, AttachmentsResponseProcessorInterface $ajax_response_attachments_processor) {
+  public function __construct(
+    ThemeManagerInterface $theme_manager,
+    ConfigFactoryInterface $config_factory,
+    ThemeInitializationInterface $theme_initializer,
+    AttachmentsResponseProcessorInterface $ajax_response_attachments_processor,
+    protected MercuryEditorContextService $mercuryEditorContext) {
     parent::__construct(NULL, 200, [], FALSE);
     $default_theme_name = $config_factory->get('system.theme')->get('default');
     $this->siteDefaultTheme = $theme_initializer->getActiveThemeByName($default_theme_name);
@@ -76,7 +84,9 @@ class IFrameAjaxResponseWrapper extends AjaxResponse {
    */
   public function addCommand(CommandInterface $command, $prepend = FALSE) {
     $this->themeManager->setActiveTheme($this->siteDefaultTheme);
+    $this->mercuryEditorContext->setPreview(TRUE);
     parent::addCommand($command, $prepend);
+    $this->mercuryEditorContext->setPreview(FALSE);
     $this->themeManager->setActiveTheme($this->currentActiveTheme);
     return $this;
   }
diff --git a/src/AjaxResponseIframeAdapter.php b/src/AjaxResponseIframeAdapter.php
deleted file mode 100644
index 4175cf7261c7b36997f24c74f67d1ab46ff2361e..0000000000000000000000000000000000000000
--- a/src/AjaxResponseIframeAdapter.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-namespace Drupal\mercury_editor;
-
-use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Theme\ThemeManagerInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Theme\ThemeInitializationInterface;
-use Drupal\mercury_editor\Ajax\IFrameCommandsWrapperCommand;
-
-class AjaxResponseIframeAdapter {
-
-  protected $defaultTheme;
-
-  protected $updatingIframe = FALSE;
-
-  protected $themeManager;
-
-  public function __construct(ThemeManagerInterface $theme_manager, ConfigFactoryInterface $config_factory, ThemeInitializationInterface $theme_initializer) {
-    $default_theme_name = $config_factory->get('system.theme')->get('default');
-    $this->defaultTheme = $theme_initializer->getActiveThemeByName($default_theme_name);
-    $this->themeManager = $theme_manager;
-  }
-
-  public function updatingIframe(){
-    $this->updatingIframe = TRUE;
-    $this->themeManager->setActiveTheme($this->defaultTheme);
-  }
-
-  public function isUpdatingIframe() {
-    return $this->updatingIframe;
-  }
-
-  public function ajaxRenderAlter(&$data) {
-    if ($this->updatingIframe) {
-      $wrapper_commands = array_filter($data, function ($command) {
-        return $command['command'] !== 'closeMercuryDialog';
-      });
-      $data = array_filter($data, function ($command) {
-        return $command['command'] == 'closeMercuryDialog';
-      });
-      $wrapper_command = new IFrameCommandsWrapperCommand($wrapper_commands);
-      $data[] = $wrapper_command->render();
-    }
-    return $data;
-  }
-
-}
diff --git a/src/Cache/MercuryEditorPreviewCacheContext.php b/src/Cache/MercuryEditorPreviewCacheContext.php
index ab591ceb9dec623e9ce2d9a0a776a66d39264cc2..7be028cfeec156b1df3d6de7ce5e58a6d29a0fe5 100644
--- a/src/Cache/MercuryEditorPreviewCacheContext.php
+++ b/src/Cache/MercuryEditorPreviewCacheContext.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\mercury_editor\Cache;
 
+use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Cache\Context\RouteNameCacheContext;
+use Drupal\mercury_editor\MercuryEditorContextService;
 
 /**
  * Determines if an entity is being viewed in Mercury Editor Preview.
@@ -14,6 +16,15 @@ use Drupal\Core\Cache\Context\RouteNameCacheContext;
  */
 class MercuryEditorPreviewCacheContext extends RouteNameCacheContext {
 
+  /**
+   * {@inheritDoc}
+   */
+  public function __construct(
+    RouteMatchInterface $route_match,
+    protected MercuryEditorContextService $mercuryEditorContext) {
+    parent::__construct($route_match);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -25,13 +36,13 @@ class MercuryEditorPreviewCacheContext extends RouteNameCacheContext {
    * {@inheritdoc}
    */
   public function getContext() {
-    $route_name = $this->routeMatch->getRouteName();
-    $is_preview = (int) (
-      $route_name == 'mercury_editor.preview'
-      || str_ends_with($route_name, '.mercury_editor_preview')
-    );
-    $context_value = 'is_mercury_editor_preview.' . $is_preview;
-    return $context_value;
+    if ($this->mercuryEditorContext->isPreview()) {
+      $entity = $this->mercuryEditorContext->getEntity();
+      return 'is_mercury_editor_preview.' . $entity->getEntityTypeId() . '.' . $entity->uuid();
+    }
+    else {
+      return 'is_mercury_editor_preview.0';
+    }
   }
 
 }
diff --git a/src/Controller/ChooseComponentController.php b/src/Controller/ChooseComponentController.php
index 32fe14c71a1a38aabb1edbdd271dcf4a6157846a..d2d873fc447392edf4b2c6dcc4782f136c3e6cc9 100644
--- a/src/Controller/ChooseComponentController.php
+++ b/src/Controller/ChooseComponentController.php
@@ -10,6 +10,7 @@ use Drupal\layout_paragraphs\LayoutParagraphsLayout;
 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Drupal\layout_paragraphs\Event\LayoutParagraphsComponentDefaultsEvent;
 use Drupal\layout_paragraphs\Controller\ChooseComponentController as LayoutParagraphsChooseComponentController;
 
 /**
@@ -64,7 +65,16 @@ class ChooseComponentController extends LayoutParagraphsChooseComponentControlle
    *   An ajax response or form render array.
    */
   protected function componentForm(string $type_name, LayoutParagraphsLayout $layout_paragraphs_layout, array $context) {
-    $type = $this->entityTypeManager()->getStorage('paragraphs_type')->load($type_name);
+
+    // Dispatch a LayoutParagraphsComponentDefaultsEvent to allow other modules
+    // to alter the paragraph type and default values.
+    $event = new LayoutParagraphsComponentDefaultsEvent($type_name, []);
+    $this->eventDispatcher->dispatch($event, $event::EVENT_NAME);
+    $type = $this
+      ->entityTypeManager()
+      ->getStorage('paragraphs_type')
+      ->load($event->getParagraphTypeId());
+
     $form = $this->formBuilder()->getForm(
       $this->getInsertComponentFormClass(),
       $layout_paragraphs_layout,
@@ -72,7 +82,8 @@ class ChooseComponentController extends LayoutParagraphsChooseComponentControlle
       $context['parent_uuid'],
       $context['region'],
       $context['sibling_uuid'],
-      $context['placement']
+      $context['placement'],
+      $event->getDefaultValues(),
     );
     if ($this->isAjax()) {
       $response = new AjaxResponse();
diff --git a/src/Controller/DuplicateController.php b/src/Controller/DuplicateController.php
new file mode 100644
index 0000000000000000000000000000000000000000..c2da11e540249d9fbfe8ed822c5b8cd99c51d483
--- /dev/null
+++ b/src/Controller/DuplicateController.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Drupal\mercury_editor\Controller;
+
+use Drupal\Core\Ajax\AfterCommand;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Ajax\AjaxHelperTrait;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\layout_paragraphs\LayoutParagraphsLayout;
+use Drupal\mercury_editor\MercuryEditorContextService;
+use Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\layout_paragraphs\Ajax\LayoutParagraphsEventCommand;
+use Drupal\layout_paragraphs\LayoutParagraphsLayoutRefreshTrait;
+use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository;
+
+/**
+ * Class DuplicateController.
+ *
+ * Duplicates a component of a Layout Paragraphs Layout.
+ * This is a copy of the DuplicateController class from the Layout Paragraphs
+ * module.
+ *
+ * @todo Consider refactoring this class to extend the original class.
+ */
+class DuplicateController extends ControllerBase {
+
+  use LayoutParagraphsLayoutRefreshTrait;
+  use AjaxHelperTrait;
+
+  /**
+   * {@inheritDoc}
+   */
+  public function __construct(
+    protected LayoutParagraphsLayoutTempstoreRepository $tempstore,
+    protected IFrameAjaxResponseWrapper $iFrameAjaxResponseWrapper,
+    protected MercuryEditorContextService $mercuryEditorContext
+    ) {}
+
+  /**
+   * {@inheritDoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('layout_paragraphs.tempstore_repository'),
+      $container->get('mercury_editor.iframe_ajax_response_wrapper'),
+      $container->get('mercury_editor.context')
+    );
+  }
+
+  /**
+   * Duplicates a component and returns appropriate response.
+   *
+   * @param \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout_paragraphs_layout
+   *   The layout paragraphs layout object.
+   * @param string $source_uuid
+   *   The source component to be cloned.
+   *
+   * @return array|\Drupal\Core\Ajax\AjaxResponse
+   *   A build array or Ajax respone.
+   */
+  public function duplicate(LayoutParagraphsLayout $layout_paragraphs_layout, string $source_uuid) {
+    $this->setLayoutParagraphsLayout($layout_paragraphs_layout);
+    $duplicate_component = $this->layoutParagraphsLayout->duplicateComponent($source_uuid);
+    $this->tempstore->set($this->layoutParagraphsLayout);
+
+    if ($this->isAjax()) {
+      $response = new AjaxResponse();
+      if ($this->needsRefresh()) {
+        $layout = $this->renderLayout();
+        $dom_selector = '[data-lpb-id="' . $this->layoutParagraphsLayout->id() . '"]';
+        $this->iFrameAjaxResponseWrapper->addCommand(new ReplaceCommand($dom_selector, $layout));
+        $response->addCommand($this->iFrameAjaxResponseWrapper->getWrapperCommand());
+        return $response;
+      }
+      $uuid = $duplicate_component->getEntity()->uuid();
+      $rendered_item = [
+        '#type' => 'layout_paragraphs_builder',
+        '#layout_paragraphs_layout' => $this->layoutParagraphsLayout,
+        '#uuid' => $uuid,
+      ];
+      $this->iFrameAjaxResponseWrapper->addCommand(new AfterCommand('[data-uuid="' . $source_uuid . '"]', $rendered_item));
+      $this->iFrameAjaxResponseWrapper->addCommand(new LayoutParagraphsEventCommand($this->layoutParagraphsLayout, $uuid, 'component:update'));
+      $response->addCommand($this->iFrameAjaxResponseWrapper->getWrapperCommand());
+      return $response;
+    }
+    return [
+      '#type' => 'layout_paragraphs_builder',
+      '#layout_paragraphs_layout' => $layout_paragraphs_layout,
+    ];
+
+  }
+
+}
diff --git a/src/Controller/InsertComponentController.php b/src/Controller/InsertComponentController.php
index 4b877046e1a1b4a6e978c970c8349e39ee820344..9b9442c1de4189e5daaee4c70f2279e1289ebe12 100644
--- a/src/Controller/InsertComponentController.php
+++ b/src/Controller/InsertComponentController.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\mercury_editor\Controller;
 
+use Drupal\Core\Form\FormState;
 use Drupal\Core\Ajax\AfterCommand;
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\AppendCommand;
@@ -10,11 +11,10 @@ use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Ajax\OpenDialogCommand;
 use Drupal\Core\Ajax\CloseDialogCommand;
 use Drupal\mercury_editor\DialogService;
-use Drupal\paragraphs\ParagraphInterface;
 use Drupal\layout_paragraphs\Utility\Dialog;
 use Symfony\Component\HttpFoundation\Request;
-use Drupal\paragraphs\ParagraphsTypeInterface;
 use Drupal\layout_paragraphs\LayoutParagraphsLayout;
+use Drupal\layout_paragraphs\Event\LayoutParagraphsComponentDefaultsEvent;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\layout_paragraphs\Ajax\LayoutParagraphsEventCommand;
 use Drupal\layout_paragraphs\Controller\ComponentFormController;
@@ -45,7 +45,9 @@ class InsertComponentController extends ComponentFormController {
   /**
    * {@inheritDoc}
    */
-  public function __construct(LayoutParagraphsLayoutTempstoreRepository $tempstore, DialogService $mercury_editor_dialog) {
+  public function __construct(
+    LayoutParagraphsLayoutTempstoreRepository $tempstore,
+    DialogService $mercury_editor_dialog) {
     $this->tempstore = $tempstore;
     $this->mercuryEditorDialog = $mercury_editor_dialog;
   }
@@ -56,15 +58,23 @@ class InsertComponentController extends ComponentFormController {
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('layout_paragraphs.tempstore_repository'),
-      $container->get('mercury_editor.dialog')
+      $container->get('mercury_editor.dialog'),
+      $container->get('event_dispatcher')
     );
   }
 
   /**
    * {@inheritDoc}
    */
-  public function skipInsertForm(Request $request, LayoutParagraphsLayout $layout_paragraphs_layout, ParagraphsTypeInterface $paragraph_type) {
+  public function skipInsertForm(Request $request, LayoutParagraphsLayout $layout_paragraphs_layout, string $paragraph_type_id) {
     $skip_for_types = $this->config('mercury_editor.settings')->get('skip_create_form');
+    $event = new LayoutParagraphsComponentDefaultsEvent($paragraph_type_id, []);
+    $this->eventDispatcher()->dispatch($event, $event::EVENT_NAME);
+    $paragraph_type = $this
+      ->entityTypeManager()
+      ->getStorage('paragraphs_type')
+      ->load($event->getParagraphTypeId());
+
     if (isset($skip_for_types[$paragraph_type->id()])) {
 
       $response = new AjaxResponse();
@@ -83,7 +93,27 @@ class InsertComponentController extends ComponentFormController {
       $paragraph = $this->entityTypeManager->getStorage('paragraph')
         ->create([$bundle_key => $paragraph_type->id()]);
 
-      $this->populateDefaultTextValues($paragraph);
+      $form_state = new FormState();
+      $args = [
+        $layout_paragraphs_layout,
+        $paragraph_type,
+        $parent_uuid,
+        $region,
+        $sibling_uuid,
+        $placement,
+      ];
+
+      $form_state
+        ->addBuildInfo('args', $args);
+      $form = $this->formBuilder()
+        ->buildForm('\Drupal\mercury_editor\Form\InsertComponentForm', $form_state);
+
+      /** @var \Drupal\mercury_editor\Form\InsertComponentForm */
+      $form_object = $form_state->getFormObject();
+      $form_state->setUserInput([]);
+      $form_object->validateForm($form, $form_state);
+      $form_object->submitForm($form, $form_state);
+      $paragraph = $form_object->buildParagraphComponent($form, $form_state);
 
       if ($sibling_uuid && $placement) {
         switch ($placement) {
@@ -133,65 +163,7 @@ class InsertComponentController extends ComponentFormController {
       $response->addCommand($iframe_ajax_response_wrapper->getWrapperCommand());
       return $response;
     }
-    return $this->insertForm($request, $layout_paragraphs_layout, $paragraph_type);
-  }
-
-  /**
-   * Populates a paragraph entity's text fields with their default values.
-   *
-   * For text fields without default values, this will insert an HTML comment
-   * placeholder to make sure the field is rendered so inline editing works.
-   *
-   * @param \Drupal\paragraphs\ParagraphInterface $paragraph
-   *   The paragraph entity.
-   */
-  protected function populateDefaultTextValues(ParagraphInterface &$paragraph) {
-
-    $field_definitions = \Drupal::service('entity_field.manager')
-      ->getFieldDefinitions('paragraph', $paragraph->bundle());
-
-    // Build an array of default field values keyed by field name.
-    $field_defaults = array_map(
-      function ($def) use ($paragraph) {
-        return $def->getDefaultValue($paragraph);
-      },
-      array_filter(
-        $field_definitions,
-        function ($def) use ($paragraph) {
-          return !empty($def->getDefaultValue($paragraph));
-        }
-      ));
-    // Build an array of text field names.
-    $field_keys = array_keys(
-      array_filter(
-        $field_definitions,
-        function ($def) use ($paragraph) {
-          return (strpos($def->getType(), 'text') === 0) && empty($def->getDefaultValue($paragraph));
-        }
-      ));
-    foreach ($field_defaults as $field_name => $default_value) {
-      $paragraph->set($field_name, $default_value);
-    }
-  }
-
-  /**
-   * {@inheritDoc}
-   *
-   * Uses mercury editor form class instead of layout paragraphs form class.
-   */
-  public function insertForm(Request $request, LayoutParagraphsLayout $layout_paragraphs_layout, ParagraphsTypeInterface | string $paragraph_type) {
-
-    $parent_uuid = $request->query->get('parent_uuid');
-    $region = $request->query->get('region');
-    $sibling_uuid = $request->query->get('sibling_uuid');
-    $placement = $request->query->get('placement');
-
-    if (is_string($paragraph_type)) {
-      $paragraph_type = $this->entityTypeManager()->getStorage('paragraphs_type')->load($paragraph_type);
-    }
-
-    $form = $this->formBuilder()->getForm('\Drupal\mercury_editor\Form\InsertComponentForm', $layout_paragraphs_layout, $paragraph_type, $parent_uuid, $region, $sibling_uuid, $placement);
-    return $this->openForm($form, $layout_paragraphs_layout);
+    return $this->insertForm($request, $layout_paragraphs_layout, $paragraph_type->id());
   }
 
   /**
@@ -224,4 +196,11 @@ class InsertComponentController extends ComponentFormController {
     return $form;
   }
 
+  /**
+   * Returns the insert component form class.
+   */
+  protected function getInsertComponentFormClass() {
+    return '\Drupal\mercury_editor\Form\InsertComponentForm';
+  }
+
 }
diff --git a/src/Controller/MercuryEditorBlockContentController.php b/src/Controller/MercuryEditorBlockContentController.php
new file mode 100644
index 0000000000000000000000000000000000000000..0b0488945ed08cdcc48c2af0fb22cf166e068a85
--- /dev/null
+++ b/src/Controller/MercuryEditorBlockContentController.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\mercury_editor\Controller;
+
+/**
+ * Provides a controller for editing block content in Mercury Editor.
+ */
+class MercuryEditorBlockContentController extends MercuryEditorController {
+
+  /**
+   * Sets the Mercury Editor preview context to true.
+   *
+   * This controller returns an empty array and expects the actual block to be
+   * rendered by the block layout system.
+   *
+   * @return array
+   *   An empty array.
+   */
+  public function preview() {
+    $this->mercuryEditorContext->setPreview(TRUE);
+    return [];
+  }
+
+}
diff --git a/src/Controller/MercuryEditorController.php b/src/Controller/MercuryEditorController.php
index a7d6e6fbf455c837060b7d85cbcc9f576b19418e..bc7c7b08d3748234a6327bf38ba8352945d4ce96 100644
--- a/src/Controller/MercuryEditorController.php
+++ b/src/Controller/MercuryEditorController.php
@@ -123,7 +123,10 @@ class MercuryEditorController extends EntityViewController {
    */
   public function preview() {
     $mercury_editor_entity = $this->mercuryEditorContext->getEntity();
-    return $this->view($mercury_editor_entity);
+    $this->mercuryEditorContext->setPreview(TRUE);
+    $preview = $this->view($mercury_editor_entity);
+    $preview['#attached']['drupalSettings']['mercuryEditorId'] = $mercury_editor_entity->uuid();
+    return $preview;
   }
 
   /**
diff --git a/src/Entity/MercuryEditorBlockContentForm.php b/src/Entity/MercuryEditorBlockContentForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..2b73d65f3b7f3e28c0b185213920ef0c5875368a
--- /dev/null
+++ b/src/Entity/MercuryEditorBlockContentForm.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace Drupal\mercury_editor\Entity;
+
+use Drupal\Core\Render\Element;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\block_content\BlockContentForm;
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Block\TitleBlockPluginInterface;
+use Drupal\Core\Plugin\ContextAwarePluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines the Mercury Editor block content form.
+ */
+class MercuryEditorBlockContentForm extends BlockContentForm {
+
+  use MercuryEditorEntityFormTrait;
+
+  /**
+   * The plugin.manager.block service.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $pluginManagerBlock;
+
+  /**
+   * The context repository service.
+   *
+   * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
+   */
+  protected $contextRepository;
+
+  /**
+   * The plugin context handler.
+   *
+   * @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
+   */
+  protected $contextHandler;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * The title resolver.
+   *
+   * @var \Drupal\Core\Controller\TitleResolverInterface
+   */
+  protected $titleResolver;
+
+  /**
+   * Injects dependencies from the container.
+   */
+  public function injectDependencies(ContainerInterface $container) {
+    $this->tempstore = $container->get('mercury_editor.tempstore_repository');
+    $this->layoutParagraphsTempstore = $container->get('layout_paragraphs.tempstore_repository');
+    $this->iFrameAjaxResponseWrapper = $container->get('mercury_editor.iframe_ajax_response_wrapper');
+    $this->entityTypeManager = $container->get('entity_type.manager');
+    $this->pluginManagerBlock = $container->get('plugin.manager.block');
+    $this->mercuryEditorContextService = $container->get('mercury_editor.context');
+    $this->contextRepository = $container->get('context.repository');
+    $this->contextHandler = $container->get('context.handler');
+    $this->routeMatch = $container->get('current_route_match');
+    $this->titleResolver = $container->get('title_resolver');
+    $this->account = $container->get('current_user');
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function setDefaultEntityValues() {
+    $this->entity->name = 'New block';
+  }
+
+  /**
+   * Ajax callback for rendering the form.
+   *
+   * @param array $form
+   *   The form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   */
+  public function ajaxCallback(array &$form, FormStateInterface $form_state) {
+    $form['#updated'] = TRUE;
+    $response = new AjaxResponse();
+
+    if (empty($form_state->getErrors())) {
+      $selector = '[data-me-edit-screen-key="' . $this->entity->uuid() . '"]';
+      $view = $this->buildBlock('block_content:' . $this->entity->uuid());
+      $this->iFrameAjaxResponseWrapper->addCommand(new ReplaceCommand($selector, $view));
+      $response->addCommand($this->iFrameAjaxResponseWrapper->getWrapperCommand());
+    }
+    else {
+      $form['#attributes']['class'][] = 'unsaved-changes';
+    }
+    $response->addCommand(new ReplaceCommand('.me-node-form', $form));
+
+    return $response;
+  }
+
+  /**
+   * Returns a block render array.
+   *
+   * @param string $id
+   *   The block id.
+   * @param array $configuration
+   *   The block configuration.
+   * @param bool $wrapper
+   *   Whether or not use block template for rendering.
+   *
+   * @return array
+   *   The built block.
+   */
+  protected function buildBlock(string $id, array $configuration = [], bool $wrapper = TRUE) {
+
+    $is_preview = $this->mercuryEditorContextService->isPreview();
+    $this->mercuryEditorContextService->setPreview(TRUE);
+
+    $configuration += ['label_display' => BlockPluginInterface::BLOCK_LABEL_VISIBLE];
+    /** @var \Drupal\Core\Block\BlockPluginInterface $block_plugin */
+    $block_plugin = $this->pluginManagerBlock->createInstance($id, $configuration);
+
+    // Inject runtime contexts.
+    if ($block_plugin instanceof ContextAwarePluginInterface) {
+      $contexts = $this->contextRepository->getRuntimeContexts($block_plugin->getContextMapping());
+      $this->contextHandler->applyContextMapping($block_plugin, $contexts);
+    }
+
+    $build = [];
+    $access = $block_plugin->access($this->account, TRUE);
+    if ($access->isAllowed()) {
+      // Title block needs a special treatment.
+      if ($block_plugin instanceof TitleBlockPluginInterface) {
+        // Account for the scenario that a NullRouteMatch is returned. This, for
+        // example, is the case when Search API is indexing the site during
+        // Drush cron.
+        if ($route = $this->routeMatch->getRouteObject()) {
+          $request = $this->requestStack->getCurrentRequest();
+          $title = $this->titleResolver->getTitle($request, $route);
+          $block_plugin->setTitle($title);
+        }
+      }
+
+      // Place the content returned by the block plugin into a 'content' child
+      // element, as a way to allow the plugin to have complete control of its
+      // properties and rendering (for instance, its own #theme) without
+      // conflicting with the properties used above.
+      $build['content'] = $block_plugin->build();
+
+      if ($block_plugin instanceof TitleBlockPluginInterface) {
+        $build['content']['#cache']['contexts'][] = 'url';
+      }
+      // Some blocks return null instead of array when empty.
+      // @see https://www.drupal.org/project/drupal/issues/3212354
+      if ($wrapper && is_array($build['content']) && !Element::isEmpty($build['content'])) {
+        $build += [
+          '#theme' => 'block',
+          '#id' => $configuration['id'] ?? NULL,
+          '#attributes' => [],
+          '#contextual_links' => [],
+          '#configuration' => $block_plugin->getConfiguration(),
+          '#plugin_id' => $block_plugin->getPluginId(),
+          '#base_plugin_id' => $block_plugin->getBaseId(),
+          '#derivative_plugin_id' => $block_plugin->getDerivativeId(),
+        ];
+        // Semantically, the content returned by the plugin is the block, and in
+        // particular, #attributes and #contextual_links is information about
+        // the *entire* block. Therefore, we must move these properties into the
+        // top-level element.
+        foreach (['#attributes', '#contextual_links'] as $property) {
+          if (isset($build['content'][$property])) {
+            $build[$property] = $build['content'][$property];
+            unset($build['content'][$property]);
+          }
+        }
+      }
+    }
+
+    CacheableMetadata::createFromRenderArray($build)
+      ->addCacheableDependency($access)
+      ->addCacheableDependency($block_plugin)
+      ->applyTo($build);
+
+    if (!isset($build['#cache']['keys'])) {
+      $build['#cache']['keys'] = [
+        'mercury_editor',
+        $id,
+        '[configuration]=' . hash('sha256', serialize($configuration)),
+        '[wrapper]=' . (int) $wrapper,
+      ];
+    }
+
+    $this->mercuryEditorContextService->setPreview($is_preview);
+    return $build;
+  }
+
+}
diff --git a/src/Entity/MercuryEditorEntityFormTrait.php b/src/Entity/MercuryEditorEntityFormTrait.php
index d3a598915fb67fa252b00a31b638a8a7a46dcbef..69907b17d220775b85126652c0a6bcd0f3d9d799 100644
--- a/src/Entity/MercuryEditorEntityFormTrait.php
+++ b/src/Entity/MercuryEditorEntityFormTrait.php
@@ -17,6 +17,14 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  */
 trait MercuryEditorEntityFormTrait {
 
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStackInterface
+   */
+  protected $requestStack;
+
+
   /**
    * The mercury editor tray tempstore repository.
    *
@@ -45,6 +53,13 @@ trait MercuryEditorEntityFormTrait {
    */
   protected $entityTypeManager;
 
+  /**
+   * The Mercury Editor context service.
+   *
+   * @var \Drupal\mercury_editor\MercuryEditorContextService
+   */
+  protected $mercuryEditorContextService;
+
   /**
    * The fields to sync changes in UI.
    *
@@ -69,6 +84,8 @@ trait MercuryEditorEntityFormTrait {
     $this->layoutParagraphsTempstore = $container->get('layout_paragraphs.tempstore_repository');
     $this->iFrameAjaxResponseWrapper = $container->get('mercury_editor.iframe_ajax_response_wrapper');
     $this->entityTypeManager = $container->get('entity_type.manager');
+    $this->mercuryEditorContextService = $container->get('mercury_editor.context');
+    $this->requestStack = $container->get('request_stack');
   }
 
   /**
@@ -153,6 +170,16 @@ trait MercuryEditorEntityFormTrait {
         // Set the Mercury Editor context on the layout.
         /** @var \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout */
         $layout = $form[$field_name]['widget']['layout_paragraphs_builder']['#layout_paragraphs_layout'];
+
+        // Set referring items so we can save the layout to the correct entity.
+        $items = $layout->getParagraphsReferenceField();
+        foreach ($items as $key => $item) {
+          if (!empty($item->entity)) {
+            $items[$key]->entity->_referringItem = $items[$key];
+          }
+        }
+        $layout->setParagraphsReferenceField($items);
+
         $settings = $layout->getSettings();
         $settings['mercury_editor_context'] = TRUE;
         $settings['is_translating'] = $form[$field_name]['widget']['layout_paragraphs_builder']['#is_translating'] ?? FALSE;
@@ -165,15 +192,15 @@ trait MercuryEditorEntityFormTrait {
     }
     if (!$form_state->get('init')) {
       $form_state->set('init', TRUE);
-      $this->tempstore->set($this->entity);
     }
+    $this->tempstore->set($this->entity);
     return $form;
   }
 
   /**
    * {@inheritdoc}
    */
-  protected function actions(array $form, FormStateInterface $form_state) {
+  protected function actions(array $form, FormStateInterface $form_state) : array {
 
     $element = parent::actions($form, $form_state);
     if (isset($element['delete'])) {
@@ -325,7 +352,12 @@ trait MercuryEditorEntityFormTrait {
    */
   public function processRedirectUrl(array $element, FormstateInterface $form_state) {
     $entity = $this->tempstore->get($this->entity->uuid());
-    if ($entity->id()) {
+    $request = $this->requestStack->getCurrentRequest();
+
+    if ($request->get('destination')) {
+      $element['#value'] = $request->get('destination');
+    }
+    elseif ($entity->id()) {
       if ($entity instanceof RevisionableInterface && ($entity->isDefaultRevision() || $entity->isLatestRevision())) {
         $element['#value'] = $entity->toUrl('canonical', ['absolute' => TRUE])->toString();
       }
@@ -336,6 +368,7 @@ trait MercuryEditorEntityFormTrait {
     else {
       $element['#value'] = Url::fromRoute('node.add_page', [], ['absolute' => TRUE])->toString();
     }
+
     return $element;
   }
 
diff --git a/src/Event/BeforeCreateComponentEvent.php b/src/Event/BeforeCreateComponentEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..abb80e5c0c68c9cf43fb7badbdd217bdcbe99fc8
--- /dev/null
+++ b/src/Event/BeforeCreateComponentEvent.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Drupal\mercury_editor\Event;
+
+use Drupal\Component\EventDispatcher\Event;
+
+/**
+ * Event that is fired before a new component is created.
+ */
+class BeforeCreateComponentEvent extends Event {
+
+  // This makes it easier for subscribers to reliably use our event name.
+  const EVENT_NAME = 'before_create_component';
+
+  /**
+   * Constructs the object.
+   *
+   * @param string $paragraphType
+   *   The paragraph type.
+   * @param array $defaultValues
+   *   The default values for the paragraph.
+   */
+  public function __construct(
+    protected string $paragraphType,
+    protected array $defaultValues) {
+  }
+
+  /**
+   * Sets the paragraph type.
+   *
+   * @param string $paragraphType
+   *   The paragraph type.
+   *
+   * @return $this
+   */
+  public function setParagraphType(string $paragraphType): self {
+    $this->paragraphType = $paragraphType;
+    return $this;
+  }
+
+  /**
+   * Gets the paragraph type.
+   *
+   * @return string
+   *   The paragraph type.
+   */
+  public function getParagraphType(): string {
+    return $this->paragraphType;
+  }
+
+  /**
+   * Sets the default values for the paragraph.
+   *
+   * @param array $defaultValues
+   *   The default values for the paragraph.
+   *
+   * @return $this
+   */
+  public function setDefaultValues(array $defaultValues): self {
+    $this->defaultValues = $defaultValues;
+    return $this;
+  }
+
+  /**
+   * Gets the default values for the paragraph.
+   *
+   * @return array
+   *   The default values for the paragraph.
+   */
+  public function getDefaultValues(): array {
+    return $this->defaultValues;
+  }
+
+}
diff --git a/src/Form/DeleteComponentForm.php b/src/Form/DeleteComponentForm.php
index 7d5837bc4c163acb1977a56dff070ae9ecf6cf56..ff12df3cf84d56f07bc5d6f4a6605860970de196 100644
--- a/src/Form/DeleteComponentForm.php
+++ b/src/Form/DeleteComponentForm.php
@@ -8,25 +8,24 @@ use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Ajax\CloseDialogCommand;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\layout_paragraphs\Utility\Dialog;
+use Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\layout_paragraphs\Ajax\LayoutParagraphsEventCommand;
+use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository;
 use Drupal\layout_paragraphs\Form\DeleteComponentForm as LayoutParagraphsDeleteComponentForm;
 
+/**
+ * Class for deleting a component in Mercury Editor.
+ */
 class DeleteComponentForm extends LayoutParagraphsDeleteComponentForm {
 
-  /**
-   * Iframe Ajax Response Wrapper service.
-   *
-   * @var \Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper
-   */
-  protected $iFrameAjaxResponseWrapper;
-
   /**
    * {@inheritDoc}
    */
-  protected function __construct($tempstore, $iframe_ajax_response_wrapper) {
-    $this->tempstore = $tempstore;
-    $this->iFrameAjaxResponseWrapper = $iframe_ajax_response_wrapper;
+  protected function __construct(
+    LayoutParagraphsLayoutTempstoreRepository $tempstore,
+    protected IFrameAjaxResponseWrapper $iFrameAjaxResponseWrapper) {
+    parent::__construct($tempstore);
   }
 
   /**
@@ -35,7 +34,8 @@ class DeleteComponentForm extends LayoutParagraphsDeleteComponentForm {
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('layout_paragraphs.tempstore_repository'),
-      $container->get('mercury_editor.iframe_ajax_response_wrapper')
+      $container->get('mercury_editor.iframe_ajax_response_wrapper'),
+      $container->get('mercury_editor.context')
     );
   }
 
diff --git a/src/Form/EditComponentForm.php b/src/Form/EditComponentForm.php
index 1192f9fd3f63278b7606b53440cbac04eb096e4b..01689bcc5d8dab6d618b703b10709f921a4b2c06 100644
--- a/src/Form/EditComponentForm.php
+++ b/src/Form/EditComponentForm.php
@@ -7,6 +7,7 @@ use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\mercury_editor\MercuryEditorTempstore;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Layout\LayoutPluginManagerInterface;
 use Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper;
@@ -16,10 +17,11 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository;
 use Drupal\layout_paragraphs\Form\EditComponentForm as LayoutParagraphsEditComponentForm;
 
+/**
+ * Class for editing a component in Mercury Editor.
+ */
 class EditComponentForm extends LayoutParagraphsEditComponentForm {
 
-  protected $iFrameAjaxResponseWrapper;
-
   /**
    * {@inheritDoc}
    */
@@ -30,10 +32,10 @@ class EditComponentForm extends LayoutParagraphsEditComponentForm {
     ModuleHandlerInterface $module_handler,
     EventDispatcherInterface $event_dispatcher,
     EntityRepositoryInterface $entity_repository,
-    IFrameAjaxResponseWrapper $iframe_ajax_response_wrapper
+    protected IFrameAjaxResponseWrapper $iFrameAjaxResponseWrapper,
+    protected MercuryEditorTempstore $mercuryEditorTempstoreRepository
     ) {
     parent::__construct($tempstore, $entity_type_manager, $layout_plugin_manager, $module_handler, $event_dispatcher, $entity_repository);
-    $this->iFrameAjaxResponseWrapper = $iframe_ajax_response_wrapper;
   }
 
   /**
@@ -47,7 +49,9 @@ class EditComponentForm extends LayoutParagraphsEditComponentForm {
       $container->get('module_handler'),
       $container->get('event_dispatcher'),
       $container->get('entity.repository'),
-      $container->get('mercury_editor.iframe_ajax_response_wrapper')
+      $container->get('mercury_editor.iframe_ajax_response_wrapper'),
+      $container->get('mercury_editor.tempstore_repository'),
+      $container->get('mercury_editor.context')
     );
   }
 
@@ -75,5 +79,4 @@ class EditComponentForm extends LayoutParagraphsEditComponentForm {
     return $response;
   }
 
-
 }
diff --git a/src/Form/InsertComponentForm.php b/src/Form/InsertComponentForm.php
index fee014e4e18554da96763cddefc5f4f7c8efc411..224d63a92825f4739b1f92fe549038fba2dbab87 100644
--- a/src/Form/InsertComponentForm.php
+++ b/src/Form/InsertComponentForm.php
@@ -9,10 +9,12 @@ use Drupal\Core\Ajax\BeforeCommand;
 use Drupal\Core\Ajax\PrependCommand;
 use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\paragraphs\ParagraphsTypeInterface;
 use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Drupal\layout_paragraphs\LayoutParagraphsLayout;
 use Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\layout_paragraphs\Ajax\LayoutParagraphsEventCommand;
@@ -20,9 +22,17 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository;
 use Drupal\layout_paragraphs\Form\InsertComponentForm as LayoutParagraphsInsertComponentForm;
 
+/**
+ * Renders a form for inserting a new component in Mercury Editor.
+ */
 class InsertComponentForm extends LayoutParagraphsInsertComponentForm {
 
-  protected $iFrameAjaxResponseWrapper;
+  /**
+   * Defaults for the paragraph.
+   *
+   * @var array
+   */
+  protected $paragraphDefaults = [];
 
   /**
    * {@inheritDoc}
@@ -34,10 +44,9 @@ class InsertComponentForm extends LayoutParagraphsInsertComponentForm {
     ModuleHandlerInterface $module_handler,
     EventDispatcherInterface $event_dispatcher,
     EntityRepositoryInterface $entity_repository,
-    IFrameAjaxResponseWrapper $iframe_ajax_response_wrapper
+    protected IFrameAjaxResponseWrapper $iFrameAjaxResponseWrapper
     ) {
     parent::__construct($tempstore, $entity_type_manager, $layout_plugin_manager, $module_handler, $event_dispatcher, $entity_repository);
-    $this->iFrameAjaxResponseWrapper = $iframe_ajax_response_wrapper;
   }
 
   /**
@@ -51,10 +60,28 @@ class InsertComponentForm extends LayoutParagraphsInsertComponentForm {
       $container->get('module_handler'),
       $container->get('event_dispatcher'),
       $container->get('entity.repository'),
-      $container->get('mercury_editor.iframe_ajax_response_wrapper')
+      $container->get('mercury_editor.iframe_ajax_response_wrapper'),
+      $container->get('mercury_editor.context')
     );
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  public function buildForm(
+    array $form,
+    FormStateInterface $form_state,
+    LayoutParagraphsLayout $layout_paragraphs_layout = NULL,
+    ParagraphsTypeInterface $paragraph_type = NULL,
+    string $parent_uuid = NULL,
+    string $region = NULL,
+    string $sibling_uuid = NULL,
+    string $placement = NULL,
+    array $paragraph_defaults = [],
+  ) {
+    $this->paragraphDefaults = $paragraph_defaults;
+    return parent::buildForm($form, $form_state, $layout_paragraphs_layout, $paragraph_type, $parent_uuid, $region, $sibling_uuid, $placement);
+  }
 
   /**
    * {@inheritDoc}
@@ -97,4 +124,23 @@ class InsertComponentForm extends LayoutParagraphsInsertComponentForm {
     return $response;
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  protected function newParagraph(ParagraphsTypeInterface $paragraph_type, string $langcode) {
+    $entity_type = $this->entityTypeManager->getDefinition('paragraph');
+    $langcode_key = $entity_type->getKey('langcode');
+    $bundle_key = $entity_type->getKey('bundle');
+    $values = [
+      $bundle_key => $paragraph_type->id(),
+      $langcode_key => $langcode,
+      '_layoutParagraphsLayout' => $this->getLayoutParagraphsLayout(),
+    ] + $this->paragraphDefaults;
+    /** @var \Drupal\paragraphs\ParagraphInterface $paragraph */
+    $paragraph = $this->entityTypeManager->getStorage('paragraph')
+      ->create($values);
+    $behavior_settings = $paragraph->getAllBehaviorSettings();
+    return $paragraph;
+  }
+
 }
diff --git a/src/MercuryEditorContextService.php b/src/MercuryEditorContextService.php
index 0340bb03c84313200e09d1c4b21efa1a07e3583c..c925a6fed8beb6f7743b1d9dd82c107a96f45f71 100644
--- a/src/MercuryEditorContextService.php
+++ b/src/MercuryEditorContextService.php
@@ -4,6 +4,10 @@ namespace Drupal\mercury_editor;
 
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Drupal\layout_paragraphs\LayoutParagraphsLayout;
+use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
+use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository;
 
 /**
  * Provides a service for Mercury Editor context.
@@ -11,21 +15,30 @@ use Drupal\Core\Entity\ContentEntityInterface;
 class MercuryEditorContextService {
 
   /**
-   * The route match.
+   * Whether the current route is a "Mercury Editor" preview.
    *
-   * @var \Drupal\Core\Routing\RouteMatchInterface
+   * @var bool
    */
-  protected RouteMatchInterface $routeMatch;
+  protected $preview = FALSE;
 
   /**
    * MercuryEditorContextService constructor.
    *
-   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch
    *   The route match.
+   * @param \Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository $layoutParagraphsTempstore
+   *   The layout paragraphs layout tempstore repository.
+   * @param \Drupal\mercury_editor\MercuryEditorTempstore $mercuryEditorTempstore
+   *   The mercury editor tempstore.
+   * @param \Drupal\Core\Routing\RequestStack $requestStack
+   *   The request.
    */
-  public function __construct(RouteMatchInterface $route_match) {
-    $this->routeMatch = $route_match;
-  }
+  public function __construct(
+    protected RouteMatchInterface $routeMatch,
+    protected LayoutParagraphsLayoutTempstoreRepository $layoutParagraphsTempstore,
+    protected MercuryEditorTempstore $mercuryEditorTempstore,
+    protected RequestStack $requestStack,
+    ) {}
 
   /**
    * Determines if the current route is a "Mercury Editor" preview.
@@ -38,11 +51,35 @@ class MercuryEditorContextService {
    *   FALSE.
    */
   public function isPreview(): bool {
-    return ($route_name = $this->routeMatch->getRouteName())
-      && (
-        $route_name === 'mercury_editor.preview'
-        || str_ends_with($route_name, '.mercury_editor_preview')
-      );
+    $route_name = $this->routeMatch->getRouteName();
+    if ($route_name == 'mercury_editor.preview') {
+      return TRUE;
+    }
+    if (str_ends_with($route_name, '.mercury_editor_preview')) {
+      return TRUE;
+    }
+    $layout_paragraphs_layout = $this->routeMatch->getParameter('layout_paragraphs_layout');
+    if ($layout_paragraphs_layout) {
+      $entity = $layout_paragraphs_layout->getEntity();
+      if (!empty($entity->lp_storage_keys)) {
+        return TRUE;
+      }
+    }
+    return $this->preview;
+  }
+
+  /**
+   * Sets the preview state for the current Mercury Editor context.
+   *
+   * @param bool $preview
+   *   The preview state, true if the current context is a preview.
+   *
+   * @return $this
+   *   The current instance.
+   */
+  public function setPreview(bool $preview = TRUE) {
+    $this->preview = $preview;
+    return $this;
   }
 
   /**
@@ -55,8 +92,8 @@ class MercuryEditorContextService {
    * returns false.
    *
    * @return bool
-   *  Returns TRUE if the current route is for a "Mercury Editor" editor; FALSE
-   *  otherwise.
+   *   Returns TRUE if the current route is for a "Mercury Editor" editor; FALSE
+   *   otherwise.
    */
   public function isEditor(): bool {
     $route_name = $this->routeMatch->getRouteName();
@@ -70,16 +107,90 @@ class MercuryEditorContextService {
    *   The mercury editor entity.
    */
   public function getEntity(): ?ContentEntityInterface {
+    if ($this->requestStack->getCurrentRequest()->query->has('me_id')) {
+      $entity = $this->mercuryEditorTempstore->get($this->requestStack->getCurrentRequest()->query->get('me_id'));
+      return $entity;
+    }
     if ($this->isEditor()) {
       return $this->routeMatch->getParameter('mercury_editor_entity');
     }
+    if ($this->isPreview()) {
+      $route_name = $this->routeMatch->getRouteName();
+      if ($route_name === 'mercury_editor.preview') {
+        return $this->routeMatch->getParameter('entity');
+      }
+      if (str_ends_with($route_name, '.mercury_editor_preview')) {
+        return $this->routeMatch->getParameter(explode('.', $route_name)[1]);
+      }
+    }
+    return NULL;
+  }
+
+  /**
+   * Sets the mercury editor entity in the tempstore.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The entity.
+   */
+  public function setEntity(ContentEntityInterface $entity) {
+    $this->mercuryEditorTempstore->set($entity);
+  }
+
+  /**
+   * Saves a layout to the parent Mercury Editor Entity in the tempstore.
+   *
+   * @param \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout
+   *   The layout paragraphs layout object.
+   */
+  public function saveLayout(LayoutParagraphsLayout $layout) {
+
+    $entity = $layout->getEntity();
+    $item_list = $layout->getParagraphsReferenceField();
 
-    if ($this->isPreview() && $route_name = $this->routeMatch->getRouteName()) {
-      $entity_type = explode('.', $route_name)[1];
-      return $this->routeMatch->getParameter($entity_type);
+    while ($entity->_referringItem) {
+      $field_name = $item_list->getName();
+      $entity->$field_name = $item_list;
+      $item_list = $entity->_referringItem->getParent();
+      $parent_entity = $item_list->getEntity();
+      if ($mercury_editor_entity = $this->mercuryEditorTempstore->get($parent_entity->uuid())) {
+        $storage_key = $mercury_editor_entity->lp_storage_keys[$field_name];
+        $layout = \Drupal::service('layout_paragraphs.tempstore_repository')->getWithStorageKey($storage_key);
+        $layout->setParagraphsReferenceField($item_list);
+        $mercury_editor_entity->$field_name = $item_list;
+        $this->layoutParagraphsTempstore->set($layout);
+        $this->mercuryEditorTempstore->set($mercury_editor_entity);
+        $parent_entity = $mercury_editor_entity;
+      }
+      $entity = $parent_entity;
     }
+  }
 
-    return NULL;
+  /**
+   * Recursively finds the child entity and saves it.
+   *
+   * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $reference_field
+   *   The paragraph entity.
+   * @param string $uuid
+   *   The uuid of the entity.
+   */
+  protected function recursivelyFindChild(EntityReferenceFieldItemListInterface &$reference_field, string $uuid) {
+
+    foreach ($reference_field as &$item) {
+      if ($item->entity->uuid() == $uuid) {
+        return $item->entity;
+      }
+      $definitions = array_filter(
+        $item->entity->getFieldDefinitions(),
+        function ($defintion) {
+          return $defintion->getType() == 'entity_reference_revisions';
+        }
+      );
+      foreach ($definitions as $definition) {
+        $field_name = $definition->getName();
+        $reference_field =& $item->entity->$field_name;
+        return $this->recursivelyFindChild($reference_field, $uuid);
+      }
+    }
   }
 
 }
diff --git a/src/Plugin/Field/FieldFormatter/LayoutParagraphsBuilder.php b/src/Plugin/Field/FieldFormatter/LayoutParagraphsBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c63bf6f67c8bfbf54c0b22f079606746db569ba
--- /dev/null
+++ b/src/Plugin/Field/FieldFormatter/LayoutParagraphsBuilder.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Drupal\mercury_editor\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\TypedData\TranslatableInterface;
+use Drupal\layout_paragraphs\LayoutParagraphsLayout;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\layout_paragraphs\LayoutParagraphsComponent;
+use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
+use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\layout_paragraphs\Access\LayoutParagraphsBuilderAccess;
+use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository;
+use Drupal\layout_paragraphs\Plugin\Field\FieldFormatter\LayoutParagraphsFormatter;
+use Drupal\mercury_editor\MercuryEditorContextService;
+
+/**
+ * Layout Paragraphs Builder field formatter.
+ *
+ * @FieldFormatter(
+ *   id = "mercury_editor_layout_paragraphs_builder",
+ *   label = @Translation("Mercury Editor Layout Paragraphs Builder"),
+ *   description = @Translation("Renders paragraphs with layout."),
+ *   field_types = {
+ *     "entity_reference_revisions"
+ *   }
+ * )
+ */
+class LayoutParagraphsBuilder extends LayoutParagraphsFormatter implements ContainerFactoryPluginInterface {
+
+  /**
+   * {@inheritDoc}
+   */
+  public function __construct(
+    $plugin_id,
+    $plugin_definition,
+    FieldDefinitionInterface $field_definition,
+    array $settings,
+    $label,
+    $view_mode,
+    array $third_party_settings,
+    LoggerChannelFactoryInterface $logger_factory,
+    EntityDisplayRepositoryInterface $entity_display_repository,
+    protected LayoutParagraphsBuilderAccess $layoutParagraphsBuilderAccess,
+    protected LayoutParagraphsLayoutTempstoreRepository $tempstore,
+    protected AccountProxyInterface $account,
+    protected MercuryEditorContextService $mercuryEditorContextService) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $logger_factory, $entity_display_repository);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['label'],
+      $configuration['view_mode'],
+      $configuration['third_party_settings'],
+      $container->get('logger.factory'),
+      $container->get('entity_display.repository'),
+      $container->get('layout_paragraphs.builder_access'),
+      $container->get('layout_paragraphs.tempstore_repository'),
+      $container->get('current_user'),
+      $container->get('mercury_editor.context')
+    );
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode = NULL) {
+    $elements = parent::viewElements($items, $langcode);
+    if (!$this->mercuryEditorContextService->isPreview() && !$this->mercuryEditorContextService->isEditor()) {
+      return $elements;
+    }
+    foreach ($items as $key => $item) {
+      if (!empty($item->entity)) {
+        $items[$key]->entity->_referringItem = $items[$key];
+      }
+    }
+    $mercury_editor_entity = $this->mercuryEditorContextService->getEntity();
+    $settings = $this->getSettings() + [
+      'reference_field_view_mode' => $this->viewMode,
+      'mercury_editor_context' => TRUE,
+      'mercury_editor_uuid' => $mercury_editor_entity->uuid(),
+    ];
+    $layout = new LayoutParagraphsLayout($items, $settings);
+    if (!$this->layoutParagraphsBuilderAccess->access($this->account, $layout)->isAllowed()) {
+      return $elements;
+    }
+    $this->tempstore->set($layout);
+    $build = [
+      '#type' => 'layout_paragraphs_builder',
+      '#layout_paragraphs_layout' => $layout,
+    ];
+    return [
+      [
+        'builder' => $build,
+      ],
+    ];
+  }
+
+}
diff --git a/src/Routing/MercuryEditorPreviewRoutes.php b/src/Routing/MercuryEditorPreviewRoutes.php
index 6be6303eebaba023a8b84296b89d26b255cf0527..4284f01c92e3e1a8ca0300423d0e14c0bbea8ccb 100644
--- a/src/Routing/MercuryEditorPreviewRoutes.php
+++ b/src/Routing/MercuryEditorPreviewRoutes.php
@@ -36,10 +36,18 @@ class MercuryEditorPreviewRoutes {
       if (!$definition->getFormClass('mercury_editor')) {
         continue;
       }
+      if ($entity_type == 'block_content') {
+        $route = '/block-content/{block_content}/mercury-editor-preview';
+        $controller = '\Drupal\mercury_editor\Controller\MercuryEditorBlockContentController::preview';
+      }
+      else {
+        $route = $definition->getLinkTemplate('canonical') . '/mercury-editor-preview';
+        $controller = '\Drupal\mercury_editor\Controller\MercuryEditorController::preview';
+      }
       $routes['entity.' . $entity_type . '.mercury_editor_preview'] = new Route(
-        $definition->getLinkTemplate('canonical') . '/mercury-editor-preview',
+        $route,
         [
-          '_controller' => '\Drupal\mercury_editor\Controller\MercuryEditorController::preview',
+          '_controller' => $controller,
         ],
         [
           '_custom_access' => '\Drupal\mercury_editor\Controller\MercuryEditorController::access',
@@ -51,7 +59,6 @@ class MercuryEditorPreviewRoutes {
               'mercury_editor_entity' => TRUE,
             ],
           ],
-          '_admin_route' => FALSE,
           '_hide_admin_toolbar' => 'TRUE',
           'no_cache' => 'TRUE',
         ]
diff --git a/templates/layout-paragraphs-builder-component-menu--mercury-editor.html.twig b/templates/layout-paragraphs-builder-component-menu--mercury-editor.html.twig
index 65b08907c29b1f869cebc55f55b9b8bb88b6c3b3..d3955ab2dbdf7d34a0146ef4c37c8698beab066c 100644
--- a/templates/layout-paragraphs-builder-component-menu--mercury-editor.html.twig
+++ b/templates/layout-paragraphs-builder-component-menu--mercury-editor.html.twig
@@ -2,32 +2,11 @@
 {{ status_messages }}
 <div{{attributes}}>
 	<h4 class="visually-hidden">{{ 'Add Item'|t }}</h4>
-	{% if all_types|length > 0 %}
+	{% if count > 0 %}
 		<div class="lpb-component-list__search">
 			<input class="lpb-component-list-search-input" type="text" placeholder="Filter items..."/>
 		</div>
 		<div class="lpb-component-list__group">
-			{% if types.layout %}
-				<div class="lpb-component-list__group--layout">
-					<h3 class="lpb-component-list__group-label">{{'Layout'|t}}</h3>
-				{% endif %}
-				{% for type in types.layout %}
-					<div class="lpb-component-list__item type-{{type.id}} is-layout">
-						<a{{type.link_attributes.setAttribute('href',type.url)}}>
-							{% if type.image %}
-								<style>
-									.lpb-component-list__item.type-{{type.id}} a::before {
-										background: url({{ type.image }});
-										background-size: cover;
-									}
-								</style>
-							{% endif %}
-							{{ type.label }}</a>
-					</div>
-				{% endfor %}
-				{% if types.layout %}
-				</div>
-			{% endif %}
 			{% for group in groups %}
 				{% if group.items|length > 0 %}
 				<div class="lpb-component-list__group--content">
diff --git a/templates/page--mercury-editor.html.twig b/templates/page--mercury-editor.html.twig
index 18f12a6ba455ea78f747ae4038c52c11ed35470c..205e56d6cb13c9d021513e2d23ff517c3d6b28e7 100644
--- a/templates/page--mercury-editor.html.twig
+++ b/templates/page--mercury-editor.html.twig
@@ -57,8 +57,7 @@
   </div>
 </div>
 <div id="me-iframe-wrapper">
-<iframe id="me-preview" width="100%" height="100%" style="border:none;" src="{{ preview_url }}">
-
+  <iframe id="me-preview" width="100%" height="100%" style="border:none;" data-src="{{ preview_url }}">
   </iframe>
 </div>
 <mercury-dialog id="me-edit-screen" hide-close-button push resizable dock="right">
diff --git a/tests/cypress/cypress/e2e/mercury-editor/mercury-editor.cy.js b/tests/cypress/cypress/e2e/mercury-editor/mercury-editor.cy.js
index 58e634ca2ff4b3d1a6ccde0b0cd6103b1e7299dc..270e7c880373756f85e4e0d9a88e734b49f3bdba 100644
--- a/tests/cypress/cypress/e2e/mercury-editor/mercury-editor.cy.js
+++ b/tests/cypress/cypress/e2e/mercury-editor/mercury-editor.cy.js
@@ -142,4 +142,24 @@ describe('Mercury Editor e2e tests.', () => {
       cy.drush('pmu mercury_editor_block_visibility_test')
   });
 
+  it('tests field validation with Mercury Editor component', () => {
+    // Install the test module.
+    cy.drush('en mercury_editor_field_validation_test');
+    // Creates a new 2-column section and attempts to save it without enterring a value for the "label" field.
+    cy.visit('/node/add/me_test_ct');
+    cy.meAddComponent('me_test_section');
+    cy.meChooseLayout('layout_twocol');
+    // Save the section without entering a value for the "label" field, wich
+    // will trigger a validation error.
+    cy.meSaveComponent().then((invalid_field) => {
+      // The "invalid_field" form field should be visible and have an error class.
+      cy.get(invalid_field)
+        .should('be.visible')
+        .and('have.class', 'error');
+    });
+    // Uninstall the test module.
+    cy.drush('cr');
+    cy.drush('pmu mercury_editor_field_validation_test');
+  });
+
 });
diff --git a/tests/cypress/package-lock.json b/tests/cypress/package-lock.json
index 7e652bf0025b069c60a44fbd44979cb63c221cc4..93c763fce952466ae3e69100d4d053ff9d8364ef 100644
--- a/tests/cypress/package-lock.json
+++ b/tests/cypress/package-lock.json
@@ -9,7 +9,7 @@
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {
-        "cypress-mercury-editor": "^0.0.29"
+        "cypress-mercury-editor": "^0.0.32"
       },
       "devDependencies": {
         "cypress": "^13.6.0",
@@ -578,9 +578,9 @@
       }
     },
     "node_modules/cypress-mercury-editor": {
-      "version": "0.0.29",
-      "resolved": "https://registry.npmjs.org/cypress-mercury-editor/-/cypress-mercury-editor-0.0.29.tgz",
-      "integrity": "sha512-5cBi/d0Y+6YGIl6sfYR+Qxx0YDBs0/4wcdGJJsFBvifjeW+JntwbFC+i4qdORZVUZ1ysuD+5T5qT90hyeiC5Fw==",
+      "version": "0.0.32",
+      "resolved": "https://registry.npmjs.org/cypress-mercury-editor/-/cypress-mercury-editor-0.0.32.tgz",
+      "integrity": "sha512-d61djmmsg8pVZS08TAR2KaGzTZA8DLtSsuwYLJpYJYp91XB2HIHNgqXE8UF822aCa9zr+ccOTt3UEtEft4zj6g==",
       "dependencies": {
         "cypress-iframe": "^1.0.1"
       }
diff --git a/tests/cypress/package.json b/tests/cypress/package.json
index cdbf2f23c08c9460327f0305fc2bd4ac2505f276..70ebee94e61e5ea6a5b994de13fcc10078d437de 100644
--- a/tests/cypress/package.json
+++ b/tests/cypress/package.json
@@ -15,6 +15,6 @@
     "cypress-iframe": "^1.0.1"
   },
   "dependencies": {
-    "cypress-mercury-editor": "^0.0.29"
+    "cypress-mercury-editor": "^0.0.32"
   }
 }
diff --git a/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.info.yml b/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..50247677945f6f7c035dc6a319f20ce17d414b78
--- /dev/null
+++ b/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.info.yml
@@ -0,0 +1,7 @@
+name: Mercury Editor Validation Test
+description: 'Setup for Mercury Editor validation tests.'
+package: 'Testing'
+type: module
+core_version_requirement: ^10 || ^11
+dependencies:
+  - mercury_editor:mercury_editor
diff --git a/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.install b/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.install
new file mode 100644
index 0000000000000000000000000000000000000000..1e4b4aea2f0a4e0294c4d442fd0cba1029a40467
--- /dev/null
+++ b/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.install
@@ -0,0 +1,56 @@
+<?php
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+
+function mercury_editor_field_validation_test_install() {
+
+  $field_storage = FieldStorageConfig::loadByName('paragraph', 'field_me_test_label');
+  if (!$field_storage) {
+    // Add a paragraphs field.
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'field_me_test_label',
+      'entity_type' => 'paragraph',
+      'type' => 'text',
+      'cardinality' => '1',
+      'required' => TRUE,
+    ]);
+    $field_storage->save();
+  }
+  $field_config = FieldConfig::loadByName('paragraph', 'me_test_section', 'field_me_test_label');
+  if (!$field_config) {
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'me_test_section',
+      'label' => 'Label (Validation Test)',
+      'required' => TRUE,
+    ]);
+    $field->save();
+  }
+
+  $form_display = \Drupal::service('entity_display.repository')->getFormDisplay('paragraph', 'me_test_section');
+  $form_display->setComponent('field_me_test_label', ['type' => 'text_textfield']);
+  $form_display->save();
+
+  $view_display = \Drupal::service('entity_display.repository')->getViewDisplay('paragraph', 'me_test_section');
+  $view_display->setComponent('field_me_test_label', ['type' => 'text_default', 'label' => 'hidden']);
+  $view_display->save();
+
+}
+
+/**
+ * Implements hook_uninstall().
+ *
+ * Delete the field config and field storage.
+ */
+function mercury_editor_field_validation_test_uninstall() {
+  $field_config = FieldConfig::loadByName('paragraph', 'me_test_section', 'field_me_test_label');
+  if ($field_config) {
+    $field_config->delete();
+  }
+  $field_storage = FieldStorageConfig::loadByName('paragraph', 'field_me_test_label');
+  if ($field_storage) {
+    $field_storage->delete();
+  }
+}
+