diff --git a/assets/stacked-images/frontblocks-stacked-images-frontend.js b/assets/stacked-images/frontblocks-stacked-images-frontend.js
new file mode 100644
index 0000000..2225436
--- /dev/null
+++ b/assets/stacked-images/frontblocks-stacked-images-frontend.js
@@ -0,0 +1,121 @@
+/**
+ * FrontBlocks Stacked Images Frontend Animation
+ *
+ * @package FrontBlocks
+ */
+
+(function () {
+ 'use strict';
+
+ // Track initialized containers to avoid re-initialization.
+ const initializedContainers = new WeakSet();
+
+ /**
+ * Initialize stacked images animation.
+ */
+ function initStackedImages() {
+ const containers = document.querySelectorAll('.frbl-stacked-images-wrapper:not(.frbl-initialized)');
+
+ containers.forEach(container => {
+ // Skip if already initialized.
+ if (initializedContainers.has(container)) {
+ return;
+ }
+
+ const direction = container.dataset.direction || 'bottom';
+ const duration = parseInt(container.dataset.duration) || 1000;
+ const delay = parseInt(container.dataset.delay) || 500;
+ const images = container.querySelectorAll('.frbl-stacked-image');
+
+ if (images.length === 0) {
+ return;
+ }
+
+ // Mark as initialized.
+ container.classList.add('frbl-initialized');
+ initializedContainers.add(container);
+
+ // Set initial position for each image based on direction.
+ images.forEach((image, index) => {
+ // Set transition FIRST.
+ image.style.transition = `opacity ${duration}ms ease-out, transform ${duration}ms ease-out`;
+ image.style.position = 'absolute';
+ image.style.top = '0';
+ image.style.left = '0';
+ image.style.width = '100%';
+ image.style.height = '100%';
+
+ // Generate random rotation between -10 and 10 degrees.
+ const randomRotation = (Math.random() * 20) - 10;
+ image.dataset.rotation = randomRotation;
+
+ // Force a reflow to ensure styles are applied.
+ void image.offsetHeight;
+
+ // Then set initial state (hidden and off-screen).
+ image.style.opacity = '0';
+
+ // Set initial transform based on direction with rotation.
+ let initialTransform = '';
+ switch (direction) {
+ case 'bottom':
+ initialTransform = `translateY(100%) rotate(${randomRotation}deg)`;
+ break;
+ case 'top':
+ initialTransform = `translateY(-100%) rotate(${randomRotation}deg)`;
+ break;
+ case 'left':
+ initialTransform = `translateX(-100%) rotate(${randomRotation}deg)`;
+ break;
+ case 'right':
+ initialTransform = `translateX(100%) rotate(${randomRotation}deg)`;
+ break;
+ }
+ image.style.transform = initialTransform;
+ });
+
+ // Animate images one by one using Intersection Observer.
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ animateImages(images, duration, delay);
+ observer.unobserve(entry.target);
+ }
+ });
+ },
+ {
+ threshold: 0.2,
+ rootMargin: '0px',
+ }
+ );
+
+ observer.observe(container);
+ });
+ }
+
+ /**
+ * Animate images sequentially.
+ *
+ * @param {NodeList} images List of image elements.
+ * @param {number} duration Animation duration.
+ * @param {number} delay Delay between images.
+ */
+ function animateImages(images, duration, delay) {
+ images.forEach((image, index) => {
+ setTimeout(() => {
+ const rotation = image.dataset.rotation || 0;
+ image.style.opacity = '1';
+ // Final state: no translation, but keep the random rotation.
+ image.style.transform = `translate(0, 0) rotate(${rotation}deg)`;
+ }, index * delay);
+ });
+ }
+
+ // Initialize on DOM ready.
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initStackedImages);
+ } else {
+ initStackedImages();
+ }
+})();
diff --git a/assets/stacked-images/frontblocks-stacked-images-option.jsx b/assets/stacked-images/frontblocks-stacked-images-option.jsx
new file mode 100644
index 0000000..6270a6b
--- /dev/null
+++ b/assets/stacked-images/frontblocks-stacked-images-option.jsx
@@ -0,0 +1,203 @@
+const { registerBlockType } = wp.blocks;
+const { Fragment, useState } = wp.element;
+const { InspectorControls, MediaUpload, MediaUploadCheck, useBlockProps } = wp.blockEditor;
+const { PanelBody, SelectControl, RangeControl, Button, Placeholder } = wp.components;
+const { __ } = wp.i18n;
+
+/**
+ * Edit component for Stacked Images block.
+ *
+ * @param {Object} props - Block properties.
+ * @return {JSX.Element} Block edit component.
+ */
+function StackedImagesEdit(props) {
+ const { attributes, setAttributes } = props;
+ const { images, direction, animationDuration, animationDelay, containerHeight } = attributes;
+
+ const blockProps = useBlockProps();
+
+ const onSelectImages = (newImages) => {
+ setAttributes({
+ images: newImages.map(img => ({
+ id: img.id,
+ url: img.url,
+ alt: img.alt || '',
+ })),
+ });
+ };
+
+ const removeImage = (indexToRemove) => {
+ const newImages = images.filter((img, index) => index !== indexToRemove);
+ setAttributes({ images: newImages });
+ };
+
+ return (
+
+
+
+
+ img.id)}
+ render={({ open }) => (
+
+ )}
+ />
+
+ {images.length > 0 && (
+
+ )}
+
+
+
+ setAttributes({ direction: value })}
+ />
+ setAttributes({ animationDuration: value })}
+ min={200}
+ max={3000}
+ step={100}
+ />
+ setAttributes({ animationDelay: value })}
+ min={0}
+ max={2000}
+ step={100}
+ />
+ setAttributes({ containerHeight: value })}
+ min={200}
+ max={1000}
+ step={50}
+ />
+
+
+
+
+
+ {images.length === 0 ? (
+
+ ) : (
+
+
+ {images.map((image, index) => (
+
+

+
+ ))}
+
+
+
+ {images.length} {images.length === 1 ? __('image', 'frontblocks') : __('images', 'frontblocks')} |
+ {__(' Direction: ', 'frontblocks')} {direction} |
+ {__(' Duration: ', 'frontblocks')} {animationDuration}ms |
+ {__(' Delay: ', 'frontblocks')} {animationDelay}ms
+
+
+
+ )}
+
+
+
+ );
+}
+
+// Register the block.
+registerBlockType('frontblocks/stacked-images', {
+ title: __('Stacked Images', 'frontblocks'),
+ description: __('Display images with stacked animation effect from different directions.', 'frontblocks'),
+ category: 'media',
+ icon: 'format-gallery',
+ keywords: [
+ __('images', 'frontblocks'),
+ __('stacked', 'frontblocks'),
+ __('animation', 'frontblocks'),
+ __('gallery', 'frontblocks'),
+ ],
+ attributes: {
+ images: {
+ type: 'array',
+ default: [],
+ },
+ direction: {
+ type: 'string',
+ default: 'bottom',
+ },
+ animationDuration: {
+ type: 'number',
+ default: 1000,
+ },
+ animationDelay: {
+ type: 'number',
+ default: 500,
+ },
+ containerHeight: {
+ type: 'number',
+ default: 500,
+ },
+ },
+ edit: StackedImagesEdit,
+ save: function () {
+ return null; // Dynamic block, render on server side.
+ },
+});
diff --git a/assets/stacked-images/frontblocks-stacked-images.css b/assets/stacked-images/frontblocks-stacked-images.css
new file mode 100644
index 0000000..6ce317b
--- /dev/null
+++ b/assets/stacked-images/frontblocks-stacked-images.css
@@ -0,0 +1,109 @@
+/**
+ * FrontBlocks Stacked Images Styles
+ *
+ * @package FrontBlocks
+ */
+
+/* Wrapper */
+.frbl-stacked-images-wrapper {
+ position: relative;
+ width: 100%;
+ overflow: visible;
+ display: block;
+ background: transparent;
+ padding: 50px;
+ margin: -50px;
+}
+
+/* Container */
+.frbl-stacked-images-container {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: visible;
+}
+
+/* Individual image */
+.frbl-stacked-image {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ will-change: transform, opacity;
+ opacity: 0;
+ visibility: hidden;
+ overflow: visible;
+}
+
+/* Show when initialized */
+.frbl-initialized .frbl-stacked-image {
+ visibility: visible;
+}
+
+.frbl-stacked-image img {
+ max-width: 100%;
+ max-height: 100%;
+ width: auto;
+ height: auto;
+ object-fit: contain;
+ display: block;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* Placeholder */
+.frbl-stacked-images-placeholder {
+ padding: 40px;
+ text-align: center;
+ background: #f0f0f0;
+ border: 2px dashed #ccc;
+ border-radius: 8px;
+ color: #666;
+ font-size: 16px;
+}
+
+/* Editor styles */
+.frbl-stacked-images-editor {
+ min-height: 200px;
+}
+
+.frbl-stacked-images-preview {
+ border: 2px dashed #ddd;
+ border-radius: 8px;
+ overflow: visible;
+ background: #fafafa;
+ position: relative;
+ padding: 50px;
+ margin: 20px;
+}
+
+.frbl-stacked-image-preview {
+ border-radius: 4px;
+ overflow: visible;
+ transition: transform 0.3s ease;
+}
+
+.frbl-stacked-image-preview:hover {
+ transform: scale(1.02) !important;
+ z-index: 999 !important;
+}
+
+.frbl-stacked-image-preview img {
+ display: block;
+ transition: all 0.3s ease;
+ pointer-events: none;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .frbl-stacked-images-wrapper {
+ height: auto !important;
+ min-height: 300px;
+ }
+}
diff --git a/assets/stacked-images/frontblocks-stacked-images.js b/assets/stacked-images/frontblocks-stacked-images.js
new file mode 100644
index 0000000..c089f6c
--- /dev/null
+++ b/assets/stacked-images/frontblocks-stacked-images.js
@@ -0,0 +1,227 @@
+"use strict";
+
+var registerBlockType = wp.blocks.registerBlockType;
+var _wp$element = wp.element,
+ Fragment = _wp$element.Fragment,
+ useState = _wp$element.useState;
+var _wp$blockEditor = wp.blockEditor,
+ InspectorControls = _wp$blockEditor.InspectorControls,
+ MediaUpload = _wp$blockEditor.MediaUpload,
+ MediaUploadCheck = _wp$blockEditor.MediaUploadCheck,
+ useBlockProps = _wp$blockEditor.useBlockProps;
+var _wp$components = wp.components,
+ PanelBody = _wp$components.PanelBody,
+ SelectControl = _wp$components.SelectControl,
+ RangeControl = _wp$components.RangeControl,
+ Button = _wp$components.Button,
+ Placeholder = _wp$components.Placeholder;
+var __ = wp.i18n.__;
+
+/**
+ * Edit component for Stacked Images block.
+ *
+ * @param {Object} props - Block properties.
+ * @return {JSX.Element} Block edit component.
+ */
+function StackedImagesEdit(props) {
+ var attributes = props.attributes,
+ setAttributes = props.setAttributes;
+ var images = attributes.images,
+ direction = attributes.direction,
+ animationDuration = attributes.animationDuration,
+ animationDelay = attributes.animationDelay,
+ containerHeight = attributes.containerHeight;
+ var blockProps = useBlockProps();
+ var onSelectImages = function onSelectImages(newImages) {
+ setAttributes({
+ images: newImages.map(function (img) {
+ return {
+ id: img.id,
+ url: img.url,
+ alt: img.alt || ''
+ };
+ })
+ });
+ };
+ var removeImage = function removeImage(indexToRemove) {
+ var newImages = images.filter(function (img, index) {
+ return index !== indexToRemove;
+ });
+ setAttributes({
+ images: newImages
+ });
+ };
+ return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(InspectorControls, null, /*#__PURE__*/React.createElement(PanelBody, {
+ title: __('Images', 'frontblocks'),
+ initialOpen: true
+ }, /*#__PURE__*/React.createElement(MediaUploadCheck, null, /*#__PURE__*/React.createElement(MediaUpload, {
+ onSelect: onSelectImages,
+ allowedTypes: ['image'],
+ multiple: true,
+ value: images.map(function (img) {
+ return img.id;
+ }),
+ render: function render(_ref) {
+ var open = _ref.open;
+ return /*#__PURE__*/React.createElement(Button, {
+ isPrimary: true,
+ onClick: open,
+ style: {
+ width: '100%',
+ marginBottom: '10px'
+ }
+ }, images.length === 0 ? __('Add Images', 'frontblocks') : __('Change Images (' + images.length + ')', 'frontblocks'));
+ }
+ })), images.length > 0 && /*#__PURE__*/React.createElement(Button, {
+ isDestructive: true,
+ onClick: function onClick() {
+ return setAttributes({
+ images: []
+ });
+ },
+ style: {
+ width: '100%'
+ }
+ }, __('Remove All Images', 'frontblocks'))), /*#__PURE__*/React.createElement(PanelBody, {
+ title: __('Settings', 'frontblocks'),
+ initialOpen: true
+ }, /*#__PURE__*/React.createElement(SelectControl, {
+ label: __('Animation Direction', 'frontblocks'),
+ value: direction,
+ options: [{
+ label: __('From Bottom', 'frontblocks'),
+ value: 'bottom'
+ }, {
+ label: __('From Top', 'frontblocks'),
+ value: 'top'
+ }, {
+ label: __('From Left', 'frontblocks'),
+ value: 'left'
+ }, {
+ label: __('From Right', 'frontblocks'),
+ value: 'right'
+ }],
+ onChange: function onChange(value) {
+ return setAttributes({
+ direction: value
+ });
+ }
+ }), /*#__PURE__*/React.createElement(RangeControl, {
+ label: __('Animation Duration (ms)', 'frontblocks'),
+ value: animationDuration,
+ onChange: function onChange(value) {
+ return setAttributes({
+ animationDuration: value
+ });
+ },
+ min: 200,
+ max: 3000,
+ step: 100
+ }), /*#__PURE__*/React.createElement(RangeControl, {
+ label: __('Delay Between Images (ms)', 'frontblocks'),
+ value: animationDelay,
+ onChange: function onChange(value) {
+ return setAttributes({
+ animationDelay: value
+ });
+ },
+ min: 0,
+ max: 2000,
+ step: 100
+ }), /*#__PURE__*/React.createElement(RangeControl, {
+ label: __('Container Height (px)', 'frontblocks'),
+ value: containerHeight,
+ onChange: function onChange(value) {
+ return setAttributes({
+ containerHeight: value
+ });
+ },
+ min: 200,
+ max: 1000,
+ step: 50
+ }))), /*#__PURE__*/React.createElement("div", blockProps, /*#__PURE__*/React.createElement("div", {
+ className: "frbl-stacked-images-editor"
+ }, images.length === 0 ? /*#__PURE__*/React.createElement(Placeholder, {
+ icon: "format-gallery",
+ label: __('Stacked Images', 'frontblocks'),
+ instructions: __('Use the settings panel on the right to add images →', 'frontblocks')
+ }) : /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
+ className: "frbl-stacked-images-preview",
+ style: {
+ height: containerHeight + 'px',
+ position: 'relative'
+ }
+ }, images.map(function (image, index) {
+ return /*#__PURE__*/React.createElement("div", {
+ key: index,
+ className: "frbl-stacked-image-preview",
+ style: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ zIndex: index + 1,
+ transform: "rotate(".concat(Math.random() * 20 - 10, "deg)")
+ }
+ }, /*#__PURE__*/React.createElement("img", {
+ src: image.url,
+ alt: image.alt,
+ style: {
+ width: '100%',
+ height: '100%',
+ objectFit: 'contain',
+ boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)'
+ }
+ }));
+ })), /*#__PURE__*/React.createElement("div", {
+ style: {
+ marginTop: '20px',
+ textAlign: 'center',
+ padding: '10px',
+ background: '#f0f0f0',
+ borderRadius: '4px'
+ }
+ }, /*#__PURE__*/React.createElement("p", {
+ style: {
+ margin: '0',
+ fontSize: '13px',
+ color: '#666'
+ }
+ }, /*#__PURE__*/React.createElement("strong", null, images.length), " ", images.length === 1 ? __('image', 'frontblocks') : __('images', 'frontblocks'), " |", __(' Direction: ', 'frontblocks'), " ", /*#__PURE__*/React.createElement("strong", null, direction), " |", __(' Duration: ', 'frontblocks'), " ", /*#__PURE__*/React.createElement("strong", null, animationDuration, "ms"), " |", __(' Delay: ', 'frontblocks'), " ", /*#__PURE__*/React.createElement("strong", null, animationDelay, "ms")))))));
+}
+
+// Register the block.
+registerBlockType('frontblocks/stacked-images', {
+ title: __('Stacked Images', 'frontblocks'),
+ description: __('Display images with stacked animation effect from different directions.', 'frontblocks'),
+ category: 'media',
+ icon: 'format-gallery',
+ keywords: [__('images', 'frontblocks'), __('stacked', 'frontblocks'), __('animation', 'frontblocks'), __('gallery', 'frontblocks')],
+ attributes: {
+ images: {
+ type: 'array',
+ default: []
+ },
+ direction: {
+ type: 'string',
+ default: 'bottom'
+ },
+ animationDuration: {
+ type: 'number',
+ default: 1000
+ },
+ animationDelay: {
+ type: 'number',
+ default: 500
+ },
+ containerHeight: {
+ type: 'number',
+ default: 500
+ }
+ },
+ edit: StackedImagesEdit,
+ save: function save() {
+ return null; // Dynamic block, render on server side.
+ }
+});
diff --git a/includes/Frontend/StackedImages.php b/includes/Frontend/StackedImages.php
new file mode 100644
index 0000000..9089c18
--- /dev/null
+++ b/includes/Frontend/StackedImages.php
@@ -0,0 +1,185 @@
+
+ * @copyright 2025 Closemarketing
+ * @version 1.0
+ */
+
+namespace FrontBlocks\Frontend;
+
+use WP_Block_Type_Registry;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * StackedImages class.
+ *
+ * @since 1.0.0
+ */
+class StackedImages {
+
+ /**
+ * Constructor.
+ */
+ public function __construct() {
+ add_action( 'init', array( $this, 'register_stacked_images_block' ), 20 );
+ add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) );
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) );
+ }
+
+ /**
+ * Enqueue frontend scripts and styles.
+ *
+ * @return void
+ */
+ public function enqueue_frontend_scripts() {
+ wp_register_style(
+ 'frontblocks-stacked-images-style',
+ FRBL_PLUGIN_URL . 'assets/stacked-images/frontblocks-stacked-images.css',
+ array(),
+ FRBL_VERSION
+ );
+
+ wp_register_script(
+ 'frontblocks-stacked-images-frontend',
+ FRBL_PLUGIN_URL . 'assets/stacked-images/frontblocks-stacked-images-frontend.js',
+ array(),
+ FRBL_VERSION,
+ true
+ );
+
+ if ( is_admin() || has_block( 'frontblocks/stacked-images' ) ) {
+ wp_enqueue_style( 'frontblocks-stacked-images-style' );
+ wp_enqueue_script( 'frontblocks-stacked-images-frontend' );
+ }
+ }
+
+ /**
+ * Enqueue block editor assets.
+ *
+ * @return void
+ */
+ public function enqueue_block_editor_assets() {
+ // Enqueue styles for editor.
+ wp_enqueue_style(
+ 'frontblocks-stacked-images-style',
+ FRBL_PLUGIN_URL . 'assets/stacked-images/frontblocks-stacked-images.css',
+ array(),
+ FRBL_VERSION
+ );
+
+ wp_enqueue_script(
+ 'frontblocks-stacked-images-option',
+ FRBL_PLUGIN_URL . 'assets/stacked-images/frontblocks-stacked-images.js',
+ array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-data', 'wp-editor', 'wp-block-editor', 'wp-compose', 'wp-i18n' ),
+ FRBL_VERSION,
+ true
+ );
+ }
+
+ /**
+ * Register the Stacked Images block.
+ *
+ * @return void
+ */
+ public function register_stacked_images_block() {
+ $args = array(
+ 'editor_script' => 'frontblocks-stacked-images-option',
+ 'render_callback' => array( $this, 'render_stacked_images_block' ),
+ 'attributes' => array(
+ 'images' => array(
+ 'type' => 'array',
+ 'default' => array(),
+ ),
+ 'direction' => array(
+ 'type' => 'string',
+ 'default' => 'bottom',
+ ),
+ 'animationDuration' => array(
+ 'type' => 'number',
+ 'default' => 1000,
+ ),
+ 'animationDelay' => array(
+ 'type' => 'number',
+ 'default' => 500,
+ ),
+ 'containerHeight' => array(
+ 'type' => 'number',
+ 'default' => 500,
+ ),
+ 'className' => array(
+ 'type' => 'string',
+ 'default' => '',
+ ),
+ ),
+ );
+
+ if ( ! WP_Block_Type_Registry::get_instance()->is_registered( 'frontblocks/stacked-images' ) ) {
+ register_block_type(
+ 'frontblocks/stacked-images',
+ $args
+ );
+ }
+ }
+
+ /**
+ * Render the Stacked Images block on frontend.
+ *
+ * @param array $attributes Block attributes.
+ * @return string HTML output.
+ */
+ public function render_stacked_images_block( $attributes ) {
+ $images = $attributes['images'] ?? array();
+ $direction = sanitize_text_field( $attributes['direction'] ?? 'bottom' );
+ $animation_duration = absint( $attributes['animationDuration'] ?? 1000 );
+ $animation_delay = absint( $attributes['animationDelay'] ?? 500 );
+ $container_height = absint( $attributes['containerHeight'] ?? 500 );
+
+ if ( empty( $images ) ) {
+ return '
' . esc_html__( 'Please add images to the Stacked Images block.', 'frontblocks' ) . '
';
+ }
+
+ $wrapper_class = 'frbl-stacked-images-wrapper';
+ if ( ! empty( $attributes['className'] ) ) {
+ $wrapper_class .= ' ' . esc_attr( $attributes['className'] );
+ }
+
+ ob_start();
+ ?>
+
+
+ $image ) : ?>
+
+
+

+
+
+
+
+