<?php
namespace DiviBooster\DiviBooster\Modules\Gallery\Masonry;

// === Configuration ===
const DIVI4_MODULE_SLUG   = 'et_pb_gallery';
const DIVI4_LAYOUT_FIELD  = 'fullwidth';
const DIVI4_MASONRY_SLUG  = 'dbdb_masonry';
const DIVI4_MASONRY_LABEL = 'Masonry';
const DIVI4_COLUMN_COUNT_FIELD = 'dbdb_masonry_column_count';
const DIVI4_COLUMN_COUNT_TABLET_FIELD = 'dbdb_masonry_column_count_tablet';
const DIVI4_COLUMN_COUNT_PHONE_FIELD = 'dbdb_masonry_column_count_phone';
const DIVI4_HORIZONTAL_GAP_FIELD = 'dbdb_masonry_horizontal_gap';
const DIVI4_HORIZONTAL_GAP_TABLET_FIELD = 'dbdb_masonry_horizontal_gap_tablet';
const DIVI4_HORIZONTAL_GAP_PHONE_FIELD = 'dbdb_masonry_horizontal_gap_phone';
const DIVI4_VERTICAL_GAP_FIELD = 'dbdb_masonry_vertical_gap';
const DIVI4_VERTICAL_GAP_TABLET_FIELD = 'dbdb_masonry_vertical_gap_tablet';
const DIVI4_VERTICAL_GAP_PHONE_FIELD = 'dbdb_masonry_vertical_gap_phone';
const DIVI4_GRID_SLIDE_IN_FIELD = 'dbdb_enable_grid_slide_in';

const DIVI5_MODULE_SLUG   = 'divi/gallery';
const DIVI5_LAYOUT_FIELD  = 'module.advanced.fullwidth';
const DIVI5_COLUMN_COUNT_ATTR = 'diviboosterMasonryColumnCount';
const DIVI5_HORIZONTAL_GAP_ATTR = 'diviboosterMasonryHorizontalGap';
const DIVI5_VERTICAL_GAP_ATTR = 'diviboosterMasonryVerticalGap';
const DIVI5_GRID_SLIDE_IN_ATTR = 'diviboosterEnableGridSlideIn';
const GRID_SLIDE_IN_OFF_CLASS = 'dbdb-grid-slide-in-off';
const DIVI5_MASONRY_COLUMNS_PRIORITY = 11;
const DIVI5_MASONRY_HORIZONTAL_GAP_PRIORITY = 12;
const DIVI5_MASONRY_VERTICAL_GAP_PRIORITY = 13;
const MASONRY_SUPPRESS_ITEM_ANIMATION_CLASS = 'dbgb-masonry-suppress-item-animation';
const COLUMN_COUNT_MIN = 1;
const COLUMN_COUNT_MAX = 10;
const COLUMN_COUNT_DEFAULT_DESKTOP = 4;
const COLUMN_COUNT_DEFAULT_TABLET = 2;
const COLUMN_COUNT_DEFAULT_PHONE = 1;
const GAP_DEFAULT = '30px';
const DIVI4_TABLET_MAX_WIDTH = 980;
const DIVI4_PHONE_MAX_WIDTH = 767;

// === Divi 4 field registration ===
add_filter('et_pb_all_fields_unprocessed_' . DIVI4_MODULE_SLUG, __NAMESPACE__ . '\\add_masonry_layout_option');
function add_masonry_layout_option($fields) {
    if (!is_array($fields)) {
        return $fields;
    }

    if (!isset($fields[DIVI4_LAYOUT_FIELD]) || !is_array($fields[DIVI4_LAYOUT_FIELD])) {
        return $fields;
    }
    if (!isset($fields[DIVI4_LAYOUT_FIELD]['options']) || !is_array($fields[DIVI4_LAYOUT_FIELD]['options'])) {
        return $fields;
    }
    // Add the option once.
    if (!isset($fields[DIVI4_LAYOUT_FIELD]['options'][DIVI4_MASONRY_SLUG])) {
        $fields[DIVI4_LAYOUT_FIELD]['options'][DIVI4_MASONRY_SLUG] = esc_html__(DIVI4_MASONRY_LABEL, 'et_builder');
    }
    // Keep option order deterministic.
    if (is_array($fields[DIVI4_LAYOUT_FIELD]['options'])) {
        asort($fields[DIVI4_LAYOUT_FIELD]['options'], SORT_NATURAL | SORT_FLAG_CASE);
    }

    // Describe the layout in the field help text.
    $desc_note = esc_html__('Select "Masonry" for a responsive masonry layout.', 'et_builder');
    $description = isset($fields[DIVI4_LAYOUT_FIELD]['description']) && is_string($fields[DIVI4_LAYOUT_FIELD]['description'])
        ? $fields[DIVI4_LAYOUT_FIELD]['description']
        : '';
    if ($description === '' || strpos($description, 'Masonry') === false) {
        $existing = trim($description);
        $fields[DIVI4_LAYOUT_FIELD]['description'] = trim(($existing ? ($existing . ' ') : '') . $desc_note);
    }

    // Keep shared gallery controls available for Masonry.
    foreach (['posts_number', 'show_pagination', 'show_title_and_caption'] as $field_key) {
        ensure_divi4_masonry_layout_visibility($fields, $field_key);
    }

    if (isset($fields['orientation']) && is_array($fields['orientation'])) {
        if (!isset($fields['orientation']['show_if_not']) || !is_array($fields['orientation']['show_if_not'])) {
            $fields['orientation']['show_if_not'] = [];
        }
        $fields['orientation']['show_if_not'][DIVI4_LAYOUT_FIELD] = DIVI4_MASONRY_SLUG;
    }

    // Register controls owned by this layout.
    if (!isset($fields[DIVI4_COLUMN_COUNT_FIELD])) {
        $fields[DIVI4_COLUMN_COUNT_FIELD] = array(
            'label'           => esc_html__('Masonry Columns', 'et_builder'),
            'type'            => 'range',
            'range_settings'  => array(
                'min'  => (string) COLUMN_COUNT_MIN,
                'max'  => (string) COLUMN_COUNT_MAX,
                'step' => '1',
            ),
            'option_category' => 'layout',
            'description'     => esc_html__('Choose the number of columns used by the masonry layout.', 'et_builder'),
            'default'         => (string) COLUMN_COUNT_DEFAULT_DESKTOP,
            'mobile_options'  => true,
            'tab_slug'        => 'advanced',
            'toggle_slug'     => 'layout',
            'show_if'         => array(DIVI4_LAYOUT_FIELD => DIVI4_MASONRY_SLUG),
        );
        $fields[DIVI4_COLUMN_COUNT_TABLET_FIELD] = array(
            'type'     => 'skip',
            'tab_slug' => 'advanced',
            'default'  => (string) COLUMN_COUNT_DEFAULT_TABLET,
        );
        $fields[DIVI4_COLUMN_COUNT_PHONE_FIELD] = array(
            'type'     => 'skip',
            'tab_slug' => 'advanced',
            'default'  => (string) COLUMN_COUNT_DEFAULT_PHONE,
        );
    }

    add_divi4_masonry_gap_fields(
        $fields,
        DIVI4_HORIZONTAL_GAP_FIELD,
        DIVI4_HORIZONTAL_GAP_TABLET_FIELD,
        DIVI4_HORIZONTAL_GAP_PHONE_FIELD,
        esc_html__('Masonry Horizontal Gap', 'et_builder'),
        esc_html__('Choose the horizontal space between masonry columns.', 'et_builder')
    );
    add_divi4_masonry_gap_fields(
        $fields,
        DIVI4_VERTICAL_GAP_FIELD,
        DIVI4_VERTICAL_GAP_TABLET_FIELD,
        DIVI4_VERTICAL_GAP_PHONE_FIELD,
        esc_html__('Masonry Vertical Gap', 'et_builder'),
        esc_html__('Choose the vertical space between masonry images.', 'et_builder')
    );

    // Place these controls immediately after the layout selector.
    move_divi4_masonry_fields_after_layout($fields);

    return $fields;
}

// Keep a field visible when Masonry is selected.
function ensure_divi4_masonry_layout_visibility(&$fields, $field_key) {
    if (!isset($fields[$field_key]) || !is_array($fields[$field_key])) {
        return;
    }
    if (isset($fields[$field_key]['show_if']) && !is_array($fields[$field_key]['show_if'])) {
        return;
    }
    if (!isset($fields[$field_key]['show_if'])) {
        $fields[$field_key]['show_if'] = [];
    }
    if (!isset($fields[$field_key]['show_if'][DIVI4_LAYOUT_FIELD])) {
        $fields[$field_key]['show_if'][DIVI4_LAYOUT_FIELD] = ['off', DIVI4_MASONRY_SLUG];
        return;
    }

    $current = $fields[$field_key]['show_if'][DIVI4_LAYOUT_FIELD];
    if (is_array($current)) {
        if (!in_array(DIVI4_MASONRY_SLUG, $current, true)) {
            $fields[$field_key]['show_if'][DIVI4_LAYOUT_FIELD][] = DIVI4_MASONRY_SLUG;
        }
    } elseif ($current !== DIVI4_MASONRY_SLUG) {
        $fields[$field_key]['show_if'][DIVI4_LAYOUT_FIELD] = [$current, DIVI4_MASONRY_SLUG];
    }
}

// Place Masonry controls next to the layout selector.
function move_divi4_masonry_fields_after_layout(&$fields) {
    if (!is_array($fields) || !array_key_exists(DIVI4_LAYOUT_FIELD, $fields)) {
        return;
    }

    $masonry_fields = array(
        DIVI4_COLUMN_COUNT_FIELD,
        DIVI4_COLUMN_COUNT_TABLET_FIELD,
        DIVI4_COLUMN_COUNT_PHONE_FIELD,
        DIVI4_HORIZONTAL_GAP_FIELD,
        DIVI4_HORIZONTAL_GAP_TABLET_FIELD,
        DIVI4_HORIZONTAL_GAP_PHONE_FIELD,
        DIVI4_VERTICAL_GAP_FIELD,
        DIVI4_VERTICAL_GAP_TABLET_FIELD,
        DIVI4_VERTICAL_GAP_PHONE_FIELD,
    );
    $owned_fields = array();
    foreach ($masonry_fields as $field_name) {
        if (array_key_exists($field_name, $fields)) {
            $owned_fields[$field_name] = $fields[$field_name];
            unset($fields[$field_name]);
        }
    }
    if (!$owned_fields) {
        return;
    }

    $ordered_fields = array();
    foreach ($fields as $field_name => $field) {
        $ordered_fields[$field_name] = $field;
        if ($field_name === DIVI4_LAYOUT_FIELD) {
            foreach ($owned_fields as $owned_field_name => $owned_field) {
                $ordered_fields[$owned_field_name] = $owned_field;
            }
        }
    }
    $fields = $ordered_fields;
}

// Register one responsive gap control and its mobile values.
function add_divi4_masonry_gap_fields(&$fields, $desktop_field, $tablet_field, $phone_field, $label, $description) {
    if (isset($fields[$desktop_field])) {
        return;
    }

    $fields[$desktop_field] = array(
        'label'           => $label,
        'type'            => 'range',
        'range_settings'  => array(
            'min'  => '0',
            'max'  => '200',
            'step' => '1',
        ),
        'allowed_units'   => array('px', '%', 'em', 'rem', 'vh', 'vw', 'vmin', 'vmax'),
        'default_unit'    => 'px',
        'option_category' => 'layout',
        'description'     => $description,
        'default'         => GAP_DEFAULT,
        'mobile_options'  => true,
        'tab_slug'        => 'advanced',
        'toggle_slug'     => 'layout',
        'show_if'         => array(DIVI4_LAYOUT_FIELD => DIVI4_MASONRY_SLUG),
    );
    $fields[$tablet_field] = array(
        'type'     => 'skip',
        'tab_slug' => 'advanced',
        'default'  => GAP_DEFAULT,
    );
    $fields[$phone_field] = array(
        'type'     => 'skip',
        'tab_slug' => 'advanced',
        'default'  => GAP_DEFAULT,
    );
}

// === Shared helpers ===
function extract_order_class_from_html($html) {
    if (!is_string($html) || $html === '') return '';
    if (!preg_match('/<div[^>]*class=\"([^\"]*\bet_pb_gallery\b[^\"]*)\"/i', $html, $m)) {
        return '';
    }
    $classes = preg_split('/\s+/', $m[1], -1, PREG_SPLIT_NO_EMPTY);
    foreach ($classes as $c) {
        if (preg_match('/^et_pb_gallery_\d+(?:_[^\s]+)?$/', $c)) {
            return $c;
        }
    }
    if (preg_match('/et_pb_gallery_\d+(?:_[^\s]+)?/', $m[1], $m2)) {
        return $m2[0];
    }
    return '';
}

function add_masonry_indicator_class_to_wrapper($content, $column_counts = array(), $slide_in_enabled = true) {
    return preg_replace_callback(
        '/<div\s+class=\"([^\"]*et_pb_gallery[^\"]*)\"/i',
        function ($matches) use ($column_counts, $slide_in_enabled) {
            $classes = trim($matches[1] . ' divibooster-gallery-masonry divibooster-gallery-true-masonry');
            if (!$slide_in_enabled && strpos(' ' . $classes . ' ', ' ' . GRID_SLIDE_IN_OFF_CLASS . ' ') === false) {
                $classes .= ' ' . GRID_SLIDE_IN_OFF_CLASS;
            }
            $attrs = '<div class="' . esc_attr($classes) . '"';

            $attrs .= ' data-dbgb-true-masonry="on"';
            foreach (array(
                'desktop' => COLUMN_COUNT_DEFAULT_DESKTOP,
                'tablet'  => COLUMN_COUNT_DEFAULT_TABLET,
                'phone'   => COLUMN_COUNT_DEFAULT_PHONE,
            ) as $breakpoint => $default) {
                $value = isset($column_counts[$breakpoint]) ? $column_counts[$breakpoint] : '';
                $attrs .= ' data-dbgb-masonry-columns-' . $breakpoint . '="' . esc_attr(sanitize_column_count($value, $default)) . '"';
            }

            return $attrs;
        },
        $content,
        1
    );
}

function sanitize_img_srcset_cropped_sizes($html) {
    return preg_replace_callback(
        '/<img([^>]+)srcset=\"([^\"]+)\"([^>]*)>/i',
        function ($matches) {
            $before = $matches[1];
            $srcset = $matches[2];
            $after  = $matches[3];
            $sources = array_map('trim', explode(',', $srcset));
            $filtered = array_filter($sources, function ($src) {
                $url = trim(explode(' ', $src)[0]);
                $url_no_query = preg_replace('/\?.*$/', '', $url);
                return !preg_match('/-\d+x\d+(?=\.[a-zA-Z]{2,5}$)/', $url_no_query);
            });
            $new_srcset = $filtered ? implode(', ', $filtered) : $srcset;
            return '<img' . $before . 'srcset="' . esc_attr($new_srcset) . '"' . $after . '>';
        },
        $html
    );
}

function disable_lazy_loading_when_gallery_starts_eagerly($html) {
    if (!is_string($html) || !preg_match_all('/<img\b[^>]*>/i', $html, $matches)) {
        return $html;
    }

    $has_eager_image = false;
    foreach ($matches[0] as $img) {
        $loading = get_img_loading_value($img);
        if ($loading !== 'lazy') {
            $has_eager_image = true;
            break;
        }
    }
    if (!$has_eager_image) {
        return $html;
    }

    return preg_replace_callback('/<img\b[^>]*>/i', function ($matches) {
        return remove_lazy_loading_attr($matches[0]);
    }, $html);
}

function get_img_loading_value($img) {
    if (!is_string($img) || !preg_match('/\sloading\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))/i', $img, $matches)) {
        return '';
    }

    $value = isset($matches[1]) && $matches[1] !== ''
        ? $matches[1]
        : (isset($matches[2]) && $matches[2] !== '' ? $matches[2] : (isset($matches[3]) ? $matches[3] : ''));

    return strtolower(trim(html_entity_decode($value, ENT_QUOTES, 'UTF-8')));
}

function remove_lazy_loading_attr($img) {
    return preg_replace_callback('/\sloading\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))/i', function ($matches) {
        $value = isset($matches[1]) && $matches[1] !== ''
            ? $matches[1]
            : (isset($matches[2]) && $matches[2] !== '' ? $matches[2] : (isset($matches[3]) ? $matches[3] : ''));

        return strtolower(trim(html_entity_decode($value, ENT_QUOTES, 'UTF-8'))) === 'lazy'
            ? ''
            : $matches[0];
    }, $img);
}

function sanitize_column_count($value, $default) {
    if ($value === '' || $value === null) {
        return (int) $default;
    }
    $count = absint($value);
    if ($count < COLUMN_COUNT_MIN) {
        return COLUMN_COUNT_MIN;
    }
    if ($count > COLUMN_COUNT_MAX) {
        return COLUMN_COUNT_MAX;
    }
    return $count;
}

function masonry_responsive_values($desktop, $tablet, $phone) {
    return array(
        'desktop' => $desktop,
        'tablet'  => $tablet,
        'phone'   => $phone,
    );
}

function masonry_column_counts_from_divi4_props($props) {
    if (!is_array($props)) {
        $props = array();
    }

    return masonry_responsive_values(
        sanitize_column_count(isset($props[DIVI4_COLUMN_COUNT_FIELD]) ? $props[DIVI4_COLUMN_COUNT_FIELD] : '', COLUMN_COUNT_DEFAULT_DESKTOP),
        sanitize_column_count(isset($props[DIVI4_COLUMN_COUNT_TABLET_FIELD]) ? $props[DIVI4_COLUMN_COUNT_TABLET_FIELD] : '', COLUMN_COUNT_DEFAULT_TABLET),
        sanitize_column_count(isset($props[DIVI4_COLUMN_COUNT_PHONE_FIELD]) ? $props[DIVI4_COLUMN_COUNT_PHONE_FIELD] : '', COLUMN_COUNT_DEFAULT_PHONE)
    );
}

function masonry_gap_values_from_divi4_props($props, $desktop_field, $tablet_field, $phone_field) {
    if (!is_array($props)) {
        $props = array();
    }

    $desktop = sanitize_gap_value(isset($props[$desktop_field]) ? $props[$desktop_field] : '', GAP_DEFAULT);
    $tablet  = sanitize_gap_value(isset($props[$tablet_field]) ? $props[$tablet_field] : '', $desktop);
    $phone   = sanitize_gap_value(isset($props[$phone_field]) ? $props[$phone_field] : '', $tablet);

    return masonry_responsive_values($desktop, $tablet, $phone);
}

function masonry_layout_values_from_divi4_props($props) {
    return array(
        'columns' => masonry_column_counts_from_divi4_props($props),
        'horizontal_gap' => masonry_gap_values_from_divi4_props(
            $props,
            DIVI4_HORIZONTAL_GAP_FIELD,
            DIVI4_HORIZONTAL_GAP_TABLET_FIELD,
            DIVI4_HORIZONTAL_GAP_PHONE_FIELD
        ),
        'vertical_gap' => masonry_gap_values_from_divi4_props(
            $props,
            DIVI4_VERTICAL_GAP_FIELD,
            DIVI4_VERTICAL_GAP_TABLET_FIELD,
            DIVI4_VERTICAL_GAP_PHONE_FIELD
        ),
    );
}

function masonry_column_counts_from_divi5_attrs($attrs) {
    if (!is_array($attrs)) {
        $attrs = array();
    }

    $custom_values = get_divi5_responsive_attr_values($attrs, DIVI5_COLUMN_COUNT_ATTR);
    $native_values = $custom_values ? array() : get_divi5_native_grid_values($attrs, 'gridColumnCount');
    $source_values = array_merge(
        masonry_responsive_values(COLUMN_COUNT_DEFAULT_DESKTOP, COLUMN_COUNT_DEFAULT_TABLET, COLUMN_COUNT_DEFAULT_PHONE),
        $native_values,
        $custom_values
    );

    return resolve_divi5_responsive_values($source_values, COLUMN_COUNT_DEFAULT_DESKTOP, __NAMESPACE__ . '\\sanitize_column_count');
}

function get_divi5_native_grid_values($attrs, $field) {
    if (!is_array($attrs)) {
        return array();
    }

    $layout = isset($attrs['galleryGrid']['decoration']['layout']) && is_array($attrs['galleryGrid']['decoration']['layout'])
        ? $attrs['galleryGrid']['decoration']['layout']
        : array();
    $values = array();

    foreach ($layout as $breakpoint => $data) {
        if (is_array($data) && isset($data['value']) && is_array($data['value']) && array_key_exists($field, $data['value'])) {
            $values[$breakpoint] = $data['value'][$field];
        } elseif (is_array($data) && array_key_exists($field, $data)) {
            $values[$breakpoint] = $data[$field];
        }
    }
    if (isset($layout['value']) && is_array($layout['value']) && array_key_exists($field, $layout['value'])) {
        $values['desktop'] = $layout['value'][$field];
    }
    if (array_key_exists($field, $layout)) {
        $values['desktop'] = $layout[$field];
    }

    return $values;
}

function get_divi5_responsive_attr_values($attrs, $attr_name) {
    if (!isset($attrs[$attr_name]) || !is_array($attrs[$attr_name])) {
        return array();
    }

    $values = array();
    foreach ($attrs[$attr_name] as $breakpoint => $data) {
        if (is_array($data) && array_key_exists('value', $data) && !is_array($data['value'])) {
            $values[$breakpoint] = $data['value'];
        } elseif (!is_array($data)) {
            $values[$breakpoint] = $data;
        }
    }
    if (array_key_exists('value', $attrs[$attr_name]) && !is_array($attrs[$attr_name]['value'])) {
        $values['desktop'] = $attrs[$attr_name]['value'];
    }

    return $values;
}

function get_divi5_breakpoint_config() {
    $class = '\\ET\\Builder\\Framework\\Breakpoint\\Breakpoint';
    if (class_exists($class) && is_callable(array($class, 'get_style_breakpoint_settings'))) {
        try {
            $settings = $class::get_style_breakpoint_settings();
            $base = is_callable(array($class, 'get_base_breakpoint_name')) ? $class::get_base_breakpoint_name() : 'desktop';
            if (is_array($settings) && $settings && is_string($base) && $base !== '') {
                return array('base' => $base, 'settings' => $settings);
            }
        } catch (\Throwable $e) {
            // Fall back to the static breakpoint map.
        }
    }

    return array(
        'base' => 'desktop',
        'settings' => array(
            'phone' => array('order' => 10, 'maxWidth' => array('value' => DIVI4_PHONE_MAX_WIDTH . 'px')),
            'tablet' => array('order' => 40, 'minWidth' => array('value' => (DIVI4_PHONE_MAX_WIDTH + 1) . 'px'), 'maxWidth' => array('value' => DIVI4_TABLET_MAX_WIDTH . 'px')),
            'desktop' => array('order' => 50, 'baseDevice' => true, 'minWidth' => array('value' => (DIVI4_TABLET_MAX_WIDTH + 1) . 'px')),
        ),
    );
}

function resolve_divi5_responsive_values($source_values, $default, $sanitizer) {
    if (!is_array($source_values)) {
        $source_values = array();
    }
    if (!is_callable($sanitizer)) {
        return array();
    }

    $config = get_divi5_breakpoint_config();
    $settings = isset($config['settings']) && is_array($config['settings']) ? $config['settings'] : array();
    if (!$settings) {
        return array();
    }

    $configured_base = isset($config['base']) && is_string($config['base']) ? $config['base'] : 'desktop';
    $base = isset($settings[$configured_base]) ? $configured_base : 'desktop';
    if (!isset($settings[$base])) {
        foreach ($settings as $breakpoint => $breakpoint_settings) {
            $base = (string) $breakpoint;
            break;
        }
    }
    $base_order = isset($settings[$base]['order']) ? (int) $settings[$base]['order'] : 50;
    $base_value = array_key_exists($base, $source_values) ? $source_values[$base] : $default;
    $resolved = array();

    foreach ($settings as $breakpoint => $breakpoint_settings) {
        $target_order = isset($breakpoint_settings['order']) ? (int) $breakpoint_settings['order'] : $base_order;
        $raw_value = $base_value;
        $best_distance = abs($target_order - $base_order);

        foreach ($source_values as $source_breakpoint => $source_value) {
            if (!isset($settings[$source_breakpoint]['order'])) {
                continue;
            }
            $source_order = (int) $settings[$source_breakpoint]['order'];
            $within_path = $target_order <= $base_order
                ? ($source_order >= $target_order && $source_order <= $base_order)
                : ($source_order <= $target_order && $source_order >= $base_order);
            $distance = abs($target_order - $source_order);
            if ($within_path && $distance <= $best_distance) {
                $raw_value = $source_value;
                $best_distance = $distance;
            }
        }

        $resolved[$breakpoint] = call_user_func($sanitizer, $raw_value, $default);
    }

    return $resolved;
}

function sanitize_gap_value($value, $default = GAP_DEFAULT) {
    $value = trim((string) $value);
    $default = trim((string) $default);
    if ($value === '') {
        return $default !== '' ? $default : GAP_DEFAULT;
    }
    if (preg_match('/^\d+(?:\.\d+)?$/', $value)) {
        $value .= 'px';
    }
    return preg_match('/^(?:0|(?:\d+(?:\.\d+)?))(?:px|em|rem|%|vw|vh|vmin|vmax)?$/i', $value)
        ? $value
        : ($default !== '' ? $default : GAP_DEFAULT);
}

function sanitize_media_query_length($value) {
    $value = trim((string) $value);
    if ($value === '') {
        return '';
    }
    if (preg_match('/^\d+(?:\.\d+)?$/', $value)) {
        $value .= 'px';
    }
    return preg_match('/^(?:0|(?:\d+(?:\.\d+)?))(?:px|em|rem|vw|vh|vmin|vmax)?$/i', $value)
        ? $value
        : '';
}

function masonry_gap_values_from_divi5_attrs($attrs, $attr_name, $native_field) {
    if (!is_array($attrs)) {
        $attrs = array();
    }

    $source_values = get_divi5_responsive_attr_values($attrs, $attr_name);
    if (!$source_values) {
        $source_values = get_divi5_native_grid_values($attrs, $native_field);
    }
    if (!$source_values) {
        $source_values = masonry_responsive_values(GAP_DEFAULT, GAP_DEFAULT, GAP_DEFAULT);
    }
    return resolve_divi5_responsive_values($source_values, GAP_DEFAULT, __NAMESPACE__ . '\\sanitize_gap_value');
}

function masonry_layout_values_from_divi5_attrs($attrs) {
    return array(
        'columns' => masonry_column_counts_from_divi5_attrs($attrs),
        'horizontal_gap' => masonry_gap_values_from_divi5_attrs($attrs, DIVI5_HORIZONTAL_GAP_ATTR, 'columnGap'),
        'vertical_gap' => masonry_gap_values_from_divi5_attrs($attrs, DIVI5_VERTICAL_GAP_ATTR, 'rowGap'),
    );
}

function breakpoint_media_query_from_settings($settings) {
    if (!is_array($settings)) {
        return '';
    }

    $parts = array();
    $min_width = isset($settings['minWidth']['value']) ? sanitize_media_query_length($settings['minWidth']['value']) : '';
    $max_width = isset($settings['maxWidth']['value']) ? sanitize_media_query_length($settings['maxWidth']['value']) : '';
    if ($min_width !== '') {
        $parts[] = '(min-width: ' . $min_width . ')';
    }
    if ($max_width !== '') {
        $parts[] = '(max-width: ' . $max_width . ')';
    }
    return $parts ? '@media only screen and ' . implode(' and ', $parts) : '';
}

function masonry_variable_declarations($layout_values, $breakpoint) {
    $layout_values = is_array($layout_values) ? $layout_values : array();
    $columns = isset($layout_values['columns']) && is_array($layout_values['columns']) && isset($layout_values['columns'][$breakpoint]) ? $layout_values['columns'][$breakpoint] : COLUMN_COUNT_DEFAULT_DESKTOP;
    $horizontal_gap = isset($layout_values['horizontal_gap']) && is_array($layout_values['horizontal_gap']) && isset($layout_values['horizontal_gap'][$breakpoint]) ? $layout_values['horizontal_gap'][$breakpoint] : GAP_DEFAULT;
    $vertical_gap = isset($layout_values['vertical_gap']) && is_array($layout_values['vertical_gap']) && isset($layout_values['vertical_gap'][$breakpoint]) ? $layout_values['vertical_gap'][$breakpoint] : GAP_DEFAULT;
    return '--dbgb-masonry-column-count:' . sanitize_column_count($columns, COLUMN_COUNT_DEFAULT_DESKTOP) . ';'
        . '--dbgb-masonry-horizontal-gap:' . sanitize_gap_value($horizontal_gap, GAP_DEFAULT) . ';'
        . '--dbgb-masonry-vertical-gap:' . sanitize_gap_value($vertical_gap, GAP_DEFAULT) . ';';
}

function build_masonry_responsive_variable_css($selector, $layout_values, $platform) {
    $css = $selector . '{' . masonry_variable_declarations($layout_values, 'desktop') . '}';
    if ($platform === 'divi4') {
        foreach (array(
            'tablet' => DIVI4_TABLET_MAX_WIDTH,
            'phone'  => DIVI4_PHONE_MAX_WIDTH,
        ) as $breakpoint => $max_width) {
            $css .= '@media only screen and (max-width: ' . $max_width . 'px){' . $selector . '{' . masonry_variable_declarations($layout_values, $breakpoint) . '}}';
        }
        return $css;
    }

    $config = get_divi5_breakpoint_config();
    $settings_by_breakpoint = isset($config['settings']) && is_array($config['settings']) ? $config['settings'] : array();
    $base = isset($config['base']) ? $config['base'] : 'desktop';
    foreach ($settings_by_breakpoint as $breakpoint => $settings) {
        if ($breakpoint === $base) {
            continue;
        }
        $media_query = breakpoint_media_query_from_settings($settings);
        if ($media_query !== '') {
            $css .= $media_query . '{' . $selector . '{' . masonry_variable_declarations($layout_values, $breakpoint) . '}}';
        }
    }
    return $css;
}

function build_masonry_css($order_class, $layout_values = array(), $platform = 'divi4') {
    if ($order_class === '') return '';
    $cls = '.' . $order_class . '.divibooster-gallery-masonry';
    $true_cls = '.' . $order_class . '.divibooster-gallery-true-masonry';
    $horizontal_gap = 'var(--dbgb-masonry-horizontal-gap, ' . GAP_DEFAULT . ')';
    $vertical_gap = 'var(--dbgb-masonry-vertical-gap, ' . GAP_DEFAULT . ')';
    if (!$layout_values) {
        $layout_values = array(
            'columns' => masonry_responsive_values(COLUMN_COUNT_DEFAULT_DESKTOP, COLUMN_COUNT_DEFAULT_TABLET, COLUMN_COUNT_DEFAULT_PHONE),
            'horizontal_gap' => masonry_responsive_values(GAP_DEFAULT, GAP_DEFAULT, GAP_DEFAULT),
            'vertical_gap' => masonry_responsive_values(GAP_DEFAULT, GAP_DEFAULT, GAP_DEFAULT),
        );
    }
    $css = build_masonry_responsive_variable_css($true_cls, $layout_values, $platform);
    $css .= "{$cls} .et_pb_gallery_items{list-style:none;margin-left:0;padding-left:0;width:100% !important;max-width:100% !important;}";
    $css .= "{$cls} .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_image,{$cls} .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_title{max-width:100% !important;width:100% !important;}";
    $css .= "{$cls} .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_image img{width:100% !important;height:auto !important;display:block;}";
    $css .= "{$true_cls} .et_pb_gallery_items.dbgb-masonry-ready{display:flex !important;column-count:unset !important;column-gap:0 !important;gap:{$horizontal_gap} !important;align-items:flex-start;}";
    $css .= "{$true_cls} .et_pb_gallery_items.dbgb-masonry-ready::after{content:none !important;display:none !important;}";
    $css .= "{$true_cls} .et_pb_gallery_items > .dbgb-masonry-stash{display:none !important;}";
    $css .= "{$true_cls} .et_pb_gallery_items > .dbgb-masonry-column{break-inside:auto !important;page-break-inside:auto !important;-webkit-column-break-inside:auto !important;-moz-column-break-inside:auto !important;display:flex !important;flex:1 1 0 !important;min-width:0 !important;width:auto !important;max-width:none !important;margin:0 !important;float:none !important;flex-direction:column;gap:{$vertical_gap} !important;}";
    $css .= "{$true_cls} .dbgb-masonry-column{display:flex !important;flex:1 1 0 !important;min-width:0 !important;width:auto !important;max-width:none !important;flex-direction:column;gap:{$vertical_gap} !important;}";
    $css .= "{$true_cls} .dbgb-masonry-column .et_pb_gallery_item{display:block !important;width:100% !important;max-width:100% !important;margin:0 !important;}";
    $css .= "{$true_cls}." . GRID_SLIDE_IN_OFF_CLASS . " .et_pb_gallery_item{-webkit-animation:none!important;-moz-animation:none!important;-o-animation:none!important;-ms-animation:none!important;animation:none!important;}";
    $css .= "{$true_cls}." . MASONRY_SUPPRESS_ITEM_ANIMATION_CLASS . " .et_pb_gallery_item{-webkit-animation:none!important;-moz-animation:none!important;-o-animation:none!important;-ms-animation:none!important;animation:none!important;opacity:1!important;}";
    $css .= '@media (prefers-reduced-motion: reduce){'
        . "{$true_cls} .et_pb_gallery_item{animation:none!important;transition:none!important;opacity:1!important;}"
        . "{$true_cls} .et_pb_gallery_item *{animation:none!important;transition:none!important;scroll-behavior:auto!important;}"
        . '}';
    return $css;
}

function build_true_masonry_script() {
    $script = <<<'JS'
<script>
(function(){
  if (window.dbgbTrueMasonryLayout) {
    window.dbgbTrueMasonryLayout();
    return;
  }

  var scheduled = false;
  var allGalleriesPending = false;
  var pendingGalleries = [];
  var suppressItemAnimationClass = 'dbgb-masonry-suppress-item-animation';
  var initialAnimationDuration = 1200;
  var initialRevealFallback = 1800;
  var pendingInitialAnimationTimer = 0;
  var observedGalleryWidths = typeof window.WeakMap === 'function' ? new window.WeakMap() : null;
  var resizeObserver = typeof window.ResizeObserver === 'function' ? new window.ResizeObserver(function(entries) {
    var changedGalleries = [];
    var scheduleAll = !observedGalleryWidths;

    Array.prototype.forEach.call(entries || [], function(entry) {
      var gallery = entry && entry.target;
      if (!gallery) {
        return;
      }

      var width = entry.contentRect && Number.isFinite(entry.contentRect.width)
        ? entry.contentRect.width
        : (gallery.getBoundingClientRect ? gallery.getBoundingClientRect().width : gallery.offsetWidth);
      width = Number.isFinite(width) ? width : 0;

      if (scheduleAll) {
        return;
      }

      var previousWidth = observedGalleryWidths.get(gallery);
      observedGalleryWidths.set(gallery, width);
      if (previousWidth !== undefined && Math.abs(previousWidth - width) >= 0.5) {
        changedGalleries.push(gallery);
      }
    });

    if (scheduleAll) {
      schedule();
    } else {
      changedGalleries.forEach(schedule);
    }
  }) : null;
  var paginationObserver = typeof window.MutationObserver === 'function' ? new window.MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      var target = mutation.target && mutation.target.nodeType === 1 ? mutation.target : mutation.target.parentElement;
      var gallery = target && target.closest ? target.closest('.divibooster-gallery-true-masonry') : null;
      if (!gallery || gallery.dataset.dbgbMasonryPaginationExpected !== 'links') {
        return;
      }
      var pagination = target.closest('.et_pb_gallery_pagination');
      if (!pagination && target !== gallery) {
        return;
      }
      pagination = pagination || gallery.querySelector('.et_pb_gallery_pagination');
      if (!pagination || !pagination.querySelector('a[data-dbgb-masonry-page]')) {
        schedule(gallery);
      }
    });
  }) : null;
  var paginationLabels = {
    previous: __DBGB_MASONRY_PREVIOUS__,
    next: __DBGB_MASONRY_NEXT__,
    page: __DBGB_MASONRY_PAGE__,
    navigation: __DBGB_MASONRY_NAVIGATION__
  };

  function getColumnCount(gallery) {
    var computed = window.getComputedStyle ? window.getComputedStyle(gallery).getPropertyValue('--dbgb-masonry-column-count') : '';
    var width = window.innerWidth || document.documentElement.clientWidth || 1024;
    var attr = width <= 767 ? 'dbgbMasonryColumnsPhone' : (width <= 980 ? 'dbgbMasonryColumnsTablet' : 'dbgbMasonryColumnsDesktop');
    var count = parseInt(computed || gallery.dataset[attr] || '1', 10);
    return Math.max(1, Math.min(10, isFinite(count) ? count : 1));
  }

  function getHorizontalGap(container) {
    return getCssVariableGap(container, '--dbgb-masonry-horizontal-gap', '30px');
  }

  function getVerticalGap(container) {
    return getCssVariableGap(container, '--dbgb-masonry-vertical-gap', '30px');
  }

  function getCssVariableGap(container, variableName, fallback) {
    if (!container || !window.getComputedStyle) {
      return fallback;
    }
    var styles = window.getComputedStyle(container);
    return sanitizeGap(styles.getPropertyValue(variableName), fallback);
  }

  function sanitizeGap(value, fallback) {
    value = String(value == null ? '' : value).trim();
    fallback = String(fallback == null || fallback === '' ? '30px' : fallback).trim();
    if (!value) {
      return fallback;
    }
    if (/^\d+(?:\.\d+)?$/.test(value)) {
      value += 'px';
    }
    return /^(?:0|(?:\d+(?:\.\d+)?))(?:px|em|rem|%|vw|vh|vmin|vmax)?$/i.test(value) ? value : fallback;
  }

  function getItems(container) {
    container.setAttribute('role', 'list');
    var existing = Array.prototype.slice.call(container.querySelectorAll(':scope > .et_pb_gallery_item, :scope > .dbgb-masonry-column > .et_pb_gallery_item, :scope > .dbgb-masonry-stash > .et_pb_gallery_item'));
    existing.sort(function(a, b) {
      var ai = parseInt(a.dataset.dbgbMasonryOriginalIndex || '0', 10);
      var bi = parseInt(b.dataset.dbgbMasonryOriginalIndex || '0', 10);
      return ai - bi;
    });
    existing.forEach(function(item, index) {
      if (!item.dataset.dbgbMasonryOriginalIndex) {
        item.dataset.dbgbMasonryOriginalIndex = String(index + 1);
      }
      item.setAttribute('role', 'listitem');
    });
    return existing;
  }

  function getPerPage(container, items) {
    var raw = container ? container.getAttribute('data-per_page') : '';

    if (!raw && items.length) {
      var image = items[0].querySelector('.et_pb_gallery_image[data-per_page]');
      raw = image ? image.getAttribute('data-per_page') : '';
    }

    var perPage = parseInt(raw, 10);
    return Number.isFinite(perPage) && perPage > 0 ? perPage : 0;
  }

  function getPaginationState(gallery, container, items) {
    var pagination = gallery.querySelector('.et_pb_gallery_pagination');
    var perPage = getPerPage(container, items);
    var totalPages = perPage > 0 && perPage < items.length ? Math.ceil(items.length / perPage) : 1;
    var currentPage = parseInt(gallery.getAttribute('data-dbgb-masonry-page') || '1', 10);

    currentPage = Number.isFinite(currentPage) ? currentPage : 1;
    currentPage = Math.max(1, Math.min(totalPages, currentPage));
    gallery.setAttribute('data-dbgb-masonry-page', String(currentPage));

    if (totalPages <= 1) {
      return {
        pagination: pagination,
        perPage: perPage,
        totalPages: totalPages,
        currentPage: 1,
        visibleItems: items,
        hiddenItems: []
      };
    }

    var start = (currentPage - 1) * perPage;
    var end = start + perPage;
    var visibleItems = items.slice(start, end);

    return {
      pagination: pagination,
      perPage: perPage,
      totalPages: totalPages,
      currentPage: currentPage,
      visibleItems: visibleItems,
      hiddenItems: items.filter(function(item) {
        return visibleItems.indexOf(item) === -1;
      })
    };
  }

  function applyPaginationVisibility(state) {
    state.visibleItems.forEach(function(item) {
      item.style.display = '';
      item.removeAttribute('aria-hidden');
    });

    state.hiddenItems.forEach(function(item) {
      item.style.display = 'none';
      item.setAttribute('aria-hidden', 'true');
    });
  }

  function renderPagination(gallery, state) {
    if (!state.pagination) {
      return;
    }

    if (state.totalPages <= 1) {
      gallery.dataset.dbgbMasonryPaginationExpected = 'empty';
      state.pagination.setAttribute('aria-hidden', 'true');
      gallery.removeAttribute('data-dbgb-masonry-focus-page');
      state.pagination.innerHTML = '';
      state.pagination.style.display = 'none';
      state.pagination.style.marginTop = '';
      return;
    }

    gallery.dataset.dbgbMasonryPaginationExpected = 'links';
    state.pagination.removeAttribute('aria-hidden');
    state.pagination.setAttribute('role', 'navigation');
    state.pagination.setAttribute('aria-label', paginationLabels.navigation);
    var html = '<ul>';
    var previousPage = Math.max(1, state.currentPage - 1);
    var nextPage = Math.min(state.totalPages, state.currentPage + 1);

    if (state.currentPage > 1) {
      html += '<li class="prev"><a href="#" data-dbgb-masonry-page="' + previousPage + '">Previous</a></li>';
    }

    for (var page = 1; page <= state.totalPages; page++) {
      html += '<li><a href="#" class="' + (page === state.currentPage ? 'active' : '') + '" data-dbgb-masonry-page="' + page + '">' + page + '</a></li>';
    }

    if (state.currentPage < state.totalPages) {
      html += '<li class="next"><a href="#" data-dbgb-masonry-page="' + nextPage + '">Next</a></li>';
    }

    html += '</ul>';

    state.pagination.style.display = '';
    state.pagination.innerHTML = html;
    var previousLink = state.pagination.querySelector('.prev a');
    var nextLink = state.pagination.querySelector('.next a');
    if (previousLink) {
      previousLink.textContent = paginationLabels.previous;
      previousLink.setAttribute('rel', 'prev');
      previousLink.setAttribute('aria-label', paginationLabels.previous);
    }
    if (nextLink) {
      nextLink.textContent = paginationLabels.next;
      nextLink.setAttribute('rel', 'next');
      nextLink.setAttribute('aria-label', paginationLabels.next);
    }
    Array.prototype.forEach.call(state.pagination.querySelectorAll('a[data-dbgb-masonry-page]'), function(link) {
      var pageNumber = link.getAttribute('data-dbgb-masonry-page');
      if (!link.closest('.prev, .next')) {
        link.setAttribute('aria-label', paginationLabels.page + ' ' + pageNumber);
      }
      if (link.classList.contains('active')) {
        link.setAttribute('aria-current', 'page');
      } else {
        link.removeAttribute('aria-current');
      }
    });
    var focusPage = gallery.getAttribute('data-dbgb-masonry-focus-page');
    if (focusPage) {
      var focusLink = null;
      Array.prototype.some.call(state.pagination.querySelectorAll('a[data-dbgb-masonry-page]'), function(candidate) {
        if (candidate.getAttribute('data-dbgb-masonry-page') === focusPage) {
          focusLink = candidate;
          return true;
        }
        return false;
      });
      if (focusLink) {
        focusLink.focus();
      }
      gallery.removeAttribute('data-dbgb-masonry-focus-page');
    }
  }

  function bindPagination(gallery) {
    if (gallery.getAttribute('data-dbgb-masonry-pagination-bound') === '1') {
      return;
    }

    gallery.setAttribute('data-dbgb-masonry-pagination-bound', '1');

    gallery.addEventListener('click', function(event) {
      var link = event.target && event.target.closest ? event.target.closest('.et_pb_gallery_pagination a[data-dbgb-masonry-page]') : null;

      if (!link || !gallery.contains(link)) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();
      var page = link.getAttribute('data-dbgb-masonry-page') || '1';
      gallery.setAttribute('data-dbgb-masonry-page', page);
      gallery.setAttribute('data-dbgb-masonry-focus-page', page);
      schedule(gallery);
    });
  }

  function prepareGalleryOrderForImageClick(event) {
    var link = event.target && event.target.closest ? event.target.closest('.et_pb_gallery_image a') : null;
    var gallery = link ? link.closest('.divibooster-gallery-true-masonry') : null;
    var container = gallery ? gallery.querySelector('.et_pb_gallery_items') : null;
    if (!gallery || !container || !gallery.contains(link)) {
      return;
    }
    var items = getItems(container);
    var paginationState = getPaginationState(gallery, container, items);
    reset(container, items);
    applyPaginationVisibility(paginationState);
    window.requestAnimationFrame(schedule);
  }

  function prepareGalleryOrderForKeyboard(event) {
    if (event.key !== 'Tab') {
      return;
    }
    var active = document.activeElement;
    var gallery = active && active.closest ? active.closest('.divibooster-gallery-true-masonry') : null;
    var container = gallery ? gallery.querySelector('.et_pb_gallery_items') : null;
    if (!gallery || !container) {
      return;
    }
    var items = getItems(container);
    var paginationState = getPaginationState(gallery, container, items);
    reset(container, items);
    applyPaginationVisibility(paginationState);
    window.requestAnimationFrame(schedule);
  }

  function reset(container, items) {
    container.classList.remove('dbgb-masonry-ready');
    delete container.dataset.dbgbMasonrySignature;
    var columns = Array.prototype.slice.call(container.querySelectorAll(':scope > .dbgb-masonry-column'));
    var stash = container.querySelector(':scope > .dbgb-masonry-stash');
    var directItems = Array.prototype.slice.call(container.querySelectorAll(':scope > .et_pb_gallery_item'));
    var needsRestore = columns.length > 0 || !!stash || directItems.length !== items.length || items.some(function(item, index) {
      return directItems[index] !== item;
    });

    if (needsRestore) {
      items.forEach(function(item) {
        container.appendChild(item);
      });
    }
    columns.forEach(function(column) {
      column.remove();
    });
    if (stash) {
      stash.remove();
    }
  }

  function waitForInitialReveal(gallery) {
    if (gallery.dataset.dbgbTrueMasonryLaidOut === '1' || gallery.dataset.dbgbMasonryInitialRevealReady === '1') {
      return true;
    }
    if (gallery.dataset.dbgbMasonryInitialRevealWaitStarted === '1') {
      return false;
    }

    if (document.readyState === 'complete') {
      gallery.dataset.dbgbMasonryInitialRevealReady = '1';
      return true;
    }

    gallery.dataset.dbgbMasonryInitialRevealWaitStarted = '1';
    var settled = false;
    var timeout = window.setTimeout(finish, initialRevealFallback);
    window.addEventListener('load', finish, { once: true });
    return false;

    function finish() {
      if (settled) {
        return;
      }
      settled = true;
      window.clearTimeout(timeout);
      gallery.dataset.dbgbMasonryInitialRevealReady = '1';
      schedule(gallery);
    }
  }

  function prefersReducedMotion() {
    return typeof window.matchMedia === 'function' &&
      window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  }

  function markGalleryLaidOut(gallery) {
    gallery.dataset.dbgbTrueMasonryLaidOut = '1';
    if (prefersReducedMotion()) {
      return;
    }
    if (!gallery.dataset.dbgbMasonryInitialAnimationUntil && !gallery.classList.contains('dbdb-grid-slide-in-off')) {
      gallery.dataset.dbgbMasonryInitialAnimationUntil = String(Date.now() + initialAnimationDuration);
    }
  }

  function shouldDelayPostInitialRelayout(gallery) {
    if (gallery.dataset.dbgbTrueMasonryLaidOut !== '1') {
      return false;
    }
    var animationUntil = parseInt(gallery.dataset.dbgbMasonryInitialAnimationUntil || '0', 10);
    var remaining = animationUntil - Date.now();
    if (remaining <= 0) {
      return false;
    }
    if (!pendingInitialAnimationTimer) {
      pendingInitialAnimationTimer = window.setTimeout(function() {
        pendingInitialAnimationTimer = 0;
        schedule(gallery);
      }, remaining + 16);
    }
    return true;
  }

  function getMasonryItemOrder(item, fallback) {
    var order = parseInt(item.dataset.dbgbMasonryOriginalIndex || '', 10);
    return Number.isFinite(order) ? order : fallback;
  }

  function planBalancedMasonry(items, count, verticalGapPixels) {
    var columnHeights = [];
    var columns = [];
    for (var i = 0; i < count; i++) {
      columnHeights.push(0);
      columns.push([]);
    }

    var measured = items.map(function(item, index) {
      return {
        item: item,
        index: index,
        order: getMasonryItemOrder(item, index + 1),
        // Round measurements to keep equal items from swapping columns.
        height: Math.max(1, Math.round(getItemHeight(item) || (index + 1)))
      };
    });

    measured.sort(function(a, b) {
      return b.height - a.height || a.order - b.order || a.index - b.index;
    });

    measured.forEach(function(entry) {
      var shortestIndex = 0;
      for (var i = 1; i < columnHeights.length; i++) {
        if (columnHeights[i] < columnHeights[shortestIndex]) {
          shortestIndex = i;
        }
      }
      var isNotFirstInColumn = columns[shortestIndex].length > 0;
      columns[shortestIndex].push(entry);
      columnHeights[shortestIndex] += entry.height + (isNotFirstInColumn ? verticalGapPixels : 0);
    });

    columns.forEach(function(column) {
      column.sort(function(a, b) {
        return a.order - b.order || a.index - b.index;
      });
    });

    columns.sort(function(a, b) {
      var aOrder = a.length ? a[0].order : Number.POSITIVE_INFINITY;
      var bOrder = b.length ? b[0].order : Number.POSITIVE_INFINITY;
      return aOrder - bOrder;
    });

    var signatureParts = [];
    columns.forEach(function(column, columnIndex) {
      column.forEach(function(entry) {
        signatureParts.push(entry.order + ':' + columnIndex + ':' + Math.round(entry.height));
      });
    });

    return {
      columns: columns.map(function(column) {
        return column.map(function(entry) {
          return entry.item;
        });
      }),
      signatureParts: signatureParts
    };
  }

  function normalizeGalleryLazyLoading(items) {
    var images = [];
    var hasEagerImage = false;

    items.forEach(function(item) {
      var img = item.querySelector && item.querySelector('img');
      if (!img) {
        return;
      }
      images.push(img);
      if (String(img.getAttribute('loading') || '').toLowerCase() !== 'lazy') {
        hasEagerImage = true;
      }
    });

    if (!hasEagerImage) {
      return;
    }

    images.forEach(function(img) {
      if (String(img.getAttribute('loading') || '').toLowerCase() !== 'lazy') {
        return;
      }
      try {
        img.loading = 'eager';
      } catch(e) {}
      img.removeAttribute('loading');
    });
  }

  function layoutGallery(gallery) {
    var container = gallery.querySelector('.et_pb_gallery_items');
    if (!container) {
      return;
    }

    var items = getItems(container);
    if (!items.length) {
      return;
    }
    normalizeGalleryLazyLoading(items);
    items.forEach(function(item, index) {
      item.setAttribute('aria-posinset', String(index + 1));
      item.setAttribute('aria-setsize', String(items.length));
    });
    if (resizeObserver && gallery.dataset.dbgbMasonryResizeObserved !== '1') {
      resizeObserver.observe(gallery);
      gallery.dataset.dbgbMasonryResizeObserved = '1';
    }

    if (!waitForInitialReveal(gallery)) {
      return;
    }

    if (shouldDelayPostInitialRelayout(gallery)) {
      return;
    }

    if (gallery.dataset.dbgbTrueMasonryLaidOut === '1') {
      gallery.classList.add(suppressItemAnimationClass);
    }

    bindPagination(gallery);

    var paginationState = getPaginationState(gallery, container, items);
    var layoutItems = paginationState.visibleItems;

    renderPagination(gallery, paginationState);

    var count = getColumnCount(gallery);
    var horizontalGap = getHorizontalGap(container);
    var verticalGap = getVerticalGap(container);
    var verticalGapPixels = gapToPixels(verticalGap, container);
    if (paginationState.pagination && paginationState.totalPages > 1) {
      paginationState.pagination.style.marginTop = verticalGap;
    }
    container.style.gap = horizontalGap;

    var signatureParts = [
      'columns:' + count,
      'hgap:' + horizontalGap,
      'vgap:' + verticalGap,
      'page:' + paginationState.currentPage,
      'perPage:' + paginationState.perPage,
      'total:' + items.length
    ];
    var planResult = planBalancedMasonry(layoutItems, count, verticalGapPixels);
    var plan = planResult.columns;
    if (count > 1) {
      planResult.signatureParts.forEach(function(part) {
        signatureParts.push(part);
      });
    }

    var signature = signatureParts.join('|');
    if (container.classList.contains('dbgb-masonry-ready') && container.dataset.dbgbMasonrySignature === signature) {
      applyPaginationVisibility(paginationState);
      markGalleryLaidOut(gallery);
      return;
    }

    reset(container, items);
    applyPaginationVisibility(paginationState);

    var columns = [];
    for (var i = 0; i < count; i++) {
      var column = document.createElement('div');
      column.className = 'dbgb-masonry-column';
      column.setAttribute('role', 'presentation');
      column.dataset.dbgbMasonryColumn = String(i + 1);
      column.style.gap = verticalGap;
      container.appendChild(column);
      columns.push(column);
    }
    plan.forEach(function(columnItems, columnIndex) {
      columnItems.forEach(function(item) {
        columns[columnIndex].appendChild(item);
      });
    });
    if (paginationState.hiddenItems.length) {
      var stash = document.createElement('div');
      stash.className = 'dbgb-masonry-stash';
      stash.hidden = true;
      stash.setAttribute('aria-hidden', 'true');
      stash.setAttribute('role', 'presentation');
      paginationState.hiddenItems.forEach(function(item) {
        stash.appendChild(item);
      });
      container.appendChild(stash);
    }
    container.dataset.dbgbMasonrySignature = signature;
    container.classList.add('dbgb-masonry-ready');
    markGalleryLaidOut(gallery);
  }

  function gapToPixels(value, context) {
    value = sanitizeGap(value, '0px');
    if (/px$/i.test(value)) {
      return parseFloat(value) || 0;
    }
    if (!context || !context.appendChild) {
      return 0;
    }
    var probe = document.createElement('div');
    probe.style.position = 'absolute';
    probe.style.visibility = 'hidden';
    probe.style.pointerEvents = 'none';
    probe.style.width = '0';
    probe.style.height = value;
    context.appendChild(probe);
    var pixels = probe.getBoundingClientRect ? probe.getBoundingClientRect().height : probe.offsetHeight;
    probe.remove();
    return Number.isFinite(pixels) ? pixels : 0;
  }

  function getItemHeight(item) {
    var rect = item.getBoundingClientRect ? item.getBoundingClientRect() : null;
    if (rect && rect.height) {
      return rect.height;
    }
    if (item.offsetHeight) {
      return item.offsetHeight;
    }
    var img = item.querySelector && item.querySelector('img');
    if (img && img.naturalWidth && img.naturalHeight) {
      return (img.naturalHeight / img.naturalWidth) * (item.offsetWidth || 1);
    }
    return 0;
  }

  function layoutAll() {
    scheduled = false;
    var galleries;
    if (allGalleriesPending) {
      allGalleriesPending = false;
      pendingGalleries.length = 0;
      galleries = Array.prototype.slice.call(document.querySelectorAll('.divibooster-gallery-true-masonry[data-dbgb-true-masonry="on"]'));
    } else {
      galleries = pendingGalleries.slice();
      pendingGalleries.length = 0;
    }
    galleries.forEach(function(gallery) {
      if (document.documentElement.contains(gallery)) {
        layoutGallery(gallery);
      }
    });
  }

  function schedule(gallery) {
    if (gallery && gallery.nodeType === 1) {
      if (!allGalleriesPending && pendingGalleries.indexOf(gallery) === -1) {
        pendingGalleries.push(gallery);
      }
    } else {
      allGalleriesPending = true;
    }
    if (scheduled) {
      return;
    }
    scheduled = true;
    window.requestAnimationFrame(layoutAll);
  }

  window.dbgbTrueMasonryLayout = schedule;

  var paginationObserverTarget = document.body || document.documentElement;
  if (paginationObserver && paginationObserverTarget) {
    paginationObserver.observe(paginationObserverTarget, { childList: true, subtree: true });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', schedule);
  } else {
    schedule();
  }

  window.addEventListener('load', function() {
    schedule();
    window.setTimeout(schedule, 50);
    window.setTimeout(schedule, 250);
  });
  window.addEventListener('resize', schedule);
  document.addEventListener('click', prepareGalleryOrderForImageClick, true);
  document.addEventListener('keydown', prepareGalleryOrderForKeyboard, true);
  if (document.fonts && document.fonts.ready) {
    document.fonts.ready.then(schedule);
  }
  document.addEventListener('load', function(event) {
    if (event.target && event.target.tagName === 'IMG' && event.target.closest) {
      var gallery = event.target.closest('.divibooster-gallery-true-masonry');
      if (gallery) {
        schedule(gallery);
      }
    }
  }, true);
})();
</script>
JS;
    return strtr($script, array(
        '__DBGB_MASONRY_PREVIOUS__' => wp_json_encode(__('Previous', 'divi-gallery-booster')),
        '__DBGB_MASONRY_NEXT__'     => wp_json_encode(__('Next', 'divi-gallery-booster')),
        '__DBGB_MASONRY_PAGE__'     => wp_json_encode(__('Page', 'divi-gallery-booster')),
        '__DBGB_MASONRY_NAVIGATION__' => wp_json_encode(__('Gallery pagination', 'divi-gallery-booster')),
    ));
}

function enqueue_true_masonry_script() {
    static $added = false;
    if ($added ||
        !function_exists('wp_register_script') ||
        !function_exists('wp_script_is') ||
        !function_exists('wp_enqueue_script') ||
        !function_exists('wp_add_inline_script')) {
        return;
    }

    $handle = 'dbgb-gallery-true-masonry';
    $script = preg_replace('#^\s*<script>\s*|\s*</script>\s*$#', '', build_true_masonry_script());
    if (!wp_script_is($handle, 'registered')) {
        wp_register_script($handle, false, array(), null, true);
    }
    wp_enqueue_script($handle);
    wp_add_inline_script($handle, $script);
    $added = true;
}

function should_enable_masonry_from_divi4_props($props) {
    if (!is_array($props)) {
        return false;
    }

    $layout = isset($props[DIVI4_LAYOUT_FIELD]) ? $props[DIVI4_LAYOUT_FIELD] : '';
    return ($layout === DIVI4_MASONRY_SLUG);
}

function should_enable_masonry_from_divi5_attrs($attrs) {
    if (!is_array($attrs)) {
        return false;
    }

    // Accept converted and native attribute shapes.
    if (isset($attrs[DIVI4_LAYOUT_FIELD]) && $attrs[DIVI4_LAYOUT_FIELD] === DIVI4_MASONRY_SLUG) {
        return true;
    }

    $v = null;
    if (isset($attrs['layout']['advanced']['fullwidth']['desktop']['value'])) {
        $v = $attrs['layout']['advanced']['fullwidth']['desktop']['value'];
    } elseif (isset($attrs['layout']['advanced']['fullwidth']['value'])) {
        $v = $attrs['layout']['advanced']['fullwidth']['value'];
    } elseif (isset($attrs['module']['advanced']['fullwidth']['desktop']['value'])) {
        $v = $attrs['module']['advanced']['fullwidth']['desktop']['value'];
    } elseif (isset($attrs['module']['advanced']['fullwidth']['value'])) {
        $v = $attrs['module']['advanced']['fullwidth']['value'];
    }
    return ($v === DIVI4_MASONRY_SLUG);
}

function is_grid_slide_in_enabled_from_divi4_props($props) {
    if (!is_array($props)) {
        return true;
    }

    return !isset($props[DIVI4_GRID_SLIDE_IN_FIELD]) || $props[DIVI4_GRID_SLIDE_IN_FIELD] !== 'off';
}

function is_grid_slide_in_enabled_from_divi5_attrs($attrs) {
    if (!is_array($attrs)) {
        return true;
    }

    if (isset($attrs[DIVI4_GRID_SLIDE_IN_FIELD]) && $attrs[DIVI4_GRID_SLIDE_IN_FIELD] === 'off') {
        return false;
    }
    $grid_slide_in_attr = isset($attrs[DIVI5_GRID_SLIDE_IN_ATTR]) ? $attrs[DIVI5_GRID_SLIDE_IN_ATTR] : null;
    if (is_array($grid_slide_in_attr)) {
        if (isset($grid_slide_in_attr['desktop']['value'])) {
            return $grid_slide_in_attr['desktop']['value'] !== 'off';
        }
        if (isset($grid_slide_in_attr['value'])) {
            return $grid_slide_in_attr['value'] !== 'off';
        }
    }
    if (is_string($grid_slide_in_attr)) {
        return $grid_slide_in_attr !== 'off';
    }
    return true;
}

add_filter('divi_module_library_register_module_attrs', __NAMESPACE__ . '\\register_divi5_masonry_layout_option', 10, 2);
function register_divi5_masonry_layout_option($module_attrs, $filter_args) {
    if (!is_array($module_attrs) || !is_array($filter_args)) {
        return $module_attrs;
    }
    if (($filter_args['name'] ?? '') !== DIVI5_MODULE_SLUG) {
        return $module_attrs;
    }
    if (function_exists('dbgb_add_divi5_gallery_layout_option')) {
        try {
            $updated_attrs = \dbgb_add_divi5_gallery_layout_option($module_attrs, DIVI4_MASONRY_SLUG, DIVI4_MASONRY_LABEL);
            if (is_array($updated_attrs)) {
                $module_attrs = $updated_attrs;
            }
        } catch (\Throwable $e) {
            return $module_attrs;
        }
    }
    $module_attrs[DIVI5_COLUMN_COUNT_ATTR] = get_divi5_masonry_column_attr_definition();
    $module_attrs[DIVI5_HORIZONTAL_GAP_ATTR] = get_divi5_masonry_gap_attr_definition(
        DIVI5_HORIZONTAL_GAP_ATTR,
        'Masonry Horizontal Gap',
        'Choose the horizontal space between masonry columns.'
    );
    $module_attrs[DIVI5_VERTICAL_GAP_ATTR] = get_divi5_masonry_gap_attr_definition(
        DIVI5_VERTICAL_GAP_ATTR,
        'Masonry Vertical Gap',
        'Choose the vertical space between masonry images.'
    );
    return $module_attrs;
}

function get_divi5_masonry_field_definition($attr_name, $label, $description, $props, $default_attr) {
    return array(
        'type' => 'object',
        'settings' => array(
            'innerContent' => array(
                'groupType' => 'group-items',
                'items' => array(
                    sanitize_title($label) . 'Field' => array(
                        'groupSlug' => 'designLayout',
                        'attrName' => $attr_name,
                        'label' => __($label, 'et_builder'),
                        'description' => __($description, 'et_builder'),
                        'features' => array(
                            'hover' => false,
                            'sticky' => false,
                            'responsive' => true,
                            'preset' => 'advanced',
                        ),
                        // Use the full path so the condition follows the selected layout.
                        'showIf' => array(
                            DIVI5_LAYOUT_FIELD => array('desktop' => array('value' => DIVI4_MASONRY_SLUG)),
                        ),
                        'render' => true,
                        'priority' => get_divi5_masonry_field_priority($attr_name),
                        'component' => array(
                            'type' => 'field',
                            'name' => 'divi/range',
                            'props' => $props,
                        ),
                        'defaultAttr' => $default_attr,
                    ),
                ),
            ),
        ),
    );
}

function get_divi5_masonry_field_priority($attr_name) {
    $priorities = array(
        DIVI5_COLUMN_COUNT_ATTR => DIVI5_MASONRY_COLUMNS_PRIORITY,
        DIVI5_HORIZONTAL_GAP_ATTR => DIVI5_MASONRY_HORIZONTAL_GAP_PRIORITY,
        DIVI5_VERTICAL_GAP_ATTR => DIVI5_MASONRY_VERTICAL_GAP_PRIORITY,
    );
    return isset($priorities[$attr_name]) ? $priorities[$attr_name] : DIVI5_MASONRY_VERTICAL_GAP_PRIORITY;
}

function get_divi5_masonry_column_attr_definition() {
    return get_divi5_masonry_field_definition(
        DIVI5_COLUMN_COUNT_ATTR,
        'Masonry Columns',
        'Choose the number of columns used by the masonry layout.',
        array(
            'min' => COLUMN_COUNT_MIN,
            'max' => COLUMN_COUNT_MAX,
            'minLimit' => COLUMN_COUNT_MIN,
            'maxLimit' => COLUMN_COUNT_MAX,
            'step' => 1,
            'allowedUnits' => array(),
            'defaultUnit' => '',
        ),
        array(
            'desktop' => array('value' => (string) COLUMN_COUNT_DEFAULT_DESKTOP),
            'tablet' => array('value' => (string) COLUMN_COUNT_DEFAULT_TABLET),
            'phone' => array('value' => (string) COLUMN_COUNT_DEFAULT_PHONE),
        )
    );
}

function get_divi5_masonry_gap_attr_definition($attr_name, $label, $description) {
    return get_divi5_masonry_field_definition(
        $attr_name,
        $label,
        $description,
        array(
            'min' => 0,
            'max' => 200,
            'step' => 1,
            'unit' => 'px',
            'defaultUnit' => 'px',
            'allowedUnits' => array('px', '%', 'em', 'rem', 'vh', 'vw', 'vmin', 'vmax'),
        ),
        array(
            'desktop' => array('value' => GAP_DEFAULT),
            'tablet' => array('value' => GAP_DEFAULT),
            'phone' => array('value' => GAP_DEFAULT),
        )
    );
}

// === Divi 4 frontend rendering ===
add_filter('et_module_shortcode_output', __NAMESPACE__ . '\\gallery_module_masonry_output', 10, 3);
function gallery_module_masonry_output($output, $render_slug, $module) {
    if (!is_string($output) || $render_slug !== DIVI4_MODULE_SLUG || !is_object($module) || !isset($module->props) || !is_array($module->props)) {
        return $output;
    }
    if (!should_enable_masonry_from_divi4_props($module->props)) {
        return $output;
    }
    // Scope generated output to this module instance.
    $order_class = extract_order_class_from_html($output);
    if ($order_class === '') {
        return $output;
    }

    $layout_values = masonry_layout_values_from_divi4_props($module->props);
    $column_counts = $layout_values['columns'];
    // Mark the wrapper and expose its responsive column settings.
    $slide_in_enabled = is_grid_slide_in_enabled_from_divi4_props($module->props);
    $output = add_masonry_indicator_class_to_wrapper($output, $column_counts, $slide_in_enabled);

    // Preserve the source candidates used by masonry.
    $output = sanitize_img_srcset_cropped_sizes($output);
    $output = disable_lazy_loading_when_gallery_starts_eagerly($output);

    // Emit instance-specific responsive styles.
    $css = build_masonry_css($order_class, $layout_values, 'divi4');
    if ($css !== '') {
        $output .= '<style>' . $css . '</style>';
    }
    enqueue_true_masonry_script();

    return $output;
}

// === Divi 5 frontend rendering ===
add_filter('render_block_' . DIVI5_MODULE_SLUG, __NAMESPACE__ . '\\divi5_gallery_masonry_render', 10, 3);
function divi5_gallery_masonry_render($block_content, $parsed_block, $block) {
    if (!is_string($block_content)) {
        return $block_content;
    }
    $attrs = is_array($parsed_block) && isset($parsed_block['attrs']) && is_array($parsed_block['attrs']) ? $parsed_block['attrs'] : [];
    if (!should_enable_masonry_from_divi5_attrs($attrs)) {
        return $block_content;
    }

    $order_class = extract_order_class_from_html($block_content);
    if ($order_class === '') {
        return $block_content;
    }

    $layout_values = masonry_layout_values_from_divi5_attrs($attrs);
    $column_counts = $layout_values['columns'];
    // Mark the wrapper and normalize image candidates.
    $slide_in_enabled = is_grid_slide_in_enabled_from_divi5_attrs($attrs);
    $block_content = add_masonry_indicator_class_to_wrapper($block_content, $column_counts, $slide_in_enabled);
    $block_content = sanitize_img_srcset_cropped_sizes($block_content);
    $block_content = disable_lazy_loading_when_gallery_starts_eagerly($block_content);

    // Emit instance-specific responsive styles.
    $css = build_masonry_css($order_class, $layout_values, 'divi5');
    if ($css !== '') {
        $block_content .= '<style>' . $css . '</style>';
    }
    enqueue_true_masonry_script();

    return $block_content;
}

// === Divi 5 Visual Builder integration ===
add_action('divi_visual_builder_assets_before_enqueue_scripts', __NAMESPACE__ . '\\enqueue_divi5_vb_js');
function enqueue_divi5_vb_js() {
    if (!function_exists('et_builder_d5_enabled') ||
        !function_exists('et_core_is_fb_enabled') ||
        !function_exists('dbdb_enqueue_inline_script_if_dependencies_available')) {
        return;
    }
    try {
        if (!\et_builder_d5_enabled() || !\et_core_is_fb_enabled()) {
            return;
        }
    } catch (\Throwable $e) {
        return;
    }

    $handle = sanitize_title('divi-booster-' . DIVI5_MODULE_SLUG . '-masonry-layout');
    try {
        \dbdb_enqueue_inline_script_if_dependencies_available($handle, get_divi5_inline_js_for_masonry(), array('lodash', array('divi-vendor-wp-hooks', 'wp-hooks')));
    } catch (\Throwable $e) {
        return;
    }
}

function get_divi5_inline_js_for_masonry() {
    $d4slug = wp_json_encode(DIVI4_MODULE_SLUG);
    $d5slug = wp_json_encode(DIVI5_MODULE_SLUG);
    $masonry = wp_json_encode(DIVI4_MASONRY_SLUG);
    $masonryLabel = wp_json_encode(__(DIVI4_MASONRY_LABEL, 'et_builder'));
    $gridSlideInAttr = wp_json_encode(DIVI5_GRID_SLIDE_IN_ATTR);
    $gridSlideInD4Field = wp_json_encode(DIVI4_GRID_SLIDE_IN_FIELD);
    $gridSlideInOffClass = wp_json_encode(GRID_SLIDE_IN_OFF_CLASS);
    $columnCountAttr = wp_json_encode(DIVI5_COLUMN_COUNT_ATTR);
    $horizontalGapAttr = wp_json_encode(DIVI5_HORIZONTAL_GAP_ATTR);
    $verticalGapAttr = wp_json_encode(DIVI5_VERTICAL_GAP_ATTR);
    $columnCountAttrDefinition = wp_json_encode(get_divi5_masonry_column_attr_definition());
    $horizontalGapAttrDefinition = wp_json_encode(get_divi5_masonry_gap_attr_definition(
        DIVI5_HORIZONTAL_GAP_ATTR,
        'Masonry Horizontal Gap',
        'Choose the horizontal space between masonry columns.'
    ));
    $verticalGapAttrDefinition = wp_json_encode(get_divi5_masonry_gap_attr_definition(
        DIVI5_VERTICAL_GAP_ATTR,
        'Masonry Vertical Gap',
        'Choose the vertical space between masonry images.'
    ));
    $breakpointConfig = wp_json_encode(get_divi5_breakpoint_config());
    $columnCountConversionMap = wp_json_encode(array(
        DIVI4_COLUMN_COUNT_FIELD          => DIVI5_COLUMN_COUNT_ATTR . '.*',
        DIVI4_COLUMN_COUNT_TABLET_FIELD   => DIVI5_COLUMN_COUNT_ATTR . '.*',
        DIVI4_COLUMN_COUNT_PHONE_FIELD    => DIVI5_COLUMN_COUNT_ATTR . '.*',
        DIVI4_HORIZONTAL_GAP_FIELD        => DIVI5_HORIZONTAL_GAP_ATTR . '.*',
        DIVI4_HORIZONTAL_GAP_TABLET_FIELD => DIVI5_HORIZONTAL_GAP_ATTR . '.*',
        DIVI4_HORIZONTAL_GAP_PHONE_FIELD  => DIVI5_HORIZONTAL_GAP_ATTR . '.*',
        DIVI4_VERTICAL_GAP_FIELD          => DIVI5_VERTICAL_GAP_ATTR . '.*',
        DIVI4_VERTICAL_GAP_TABLET_FIELD   => DIVI5_VERTICAL_GAP_ATTR . '.*',
        DIVI4_VERTICAL_GAP_PHONE_FIELD    => DIVI5_VERTICAL_GAP_ATTR . '.*',
    ));
    $descNote = wp_json_encode(__('Select "Masonry" for a responsive masonry layout.', 'et_builder'));
    return <<<JS
(function(){
  const hooks = window.vendor?.wp?.hooks;
  if (!hooks) return;

  const masonryPreviewSlideInState = new Map();
  const masonryPreviewBaseOrderState = new WeakMap();
  const masonryBreakpointConfig = {$breakpointConfig};
  const masonryGridLayoutFieldNames = new Set([
    'gridEditorLauncher',
    'display',
    'columnGap',
    'rowGap',
    'gridColumnWidths',
    'gridColumnCount',
    'collapseEmptyColumns',
    'gridColumnMinWidth',
    'gridColumnWidth',
    'gridTemplateColumns',
    'gridAutoColumns',
    'gridRowHeights',
    'gridRowCount',
    'gridRowMinHeight',
    'gridRowHeight',
    'gridTemplateRows',
    'gridAutoRows',
    'gridAutoFlow',
    'gridDensity',
    'justifyContent',
    'alignItems',
    'alignContent',
    'gridJustifyItems',
    'gridOffsetRules'
  ]);
  const masonryGridVisibilityWrappedFields = new WeakSet();

  function getLayoutValueFromAttrs(attrs) {
    if (!attrs || typeof attrs !== 'object') {
      return undefined;
    }
    return (
      attrs?.layout?.advanced?.fullwidth?.desktop?.value ??
      attrs?.layout?.advanced?.fullwidth?.value ??
      attrs?.module?.advanced?.fullwidth?.desktop?.value ??
      attrs?.module?.advanced?.fullwidth?.value ??
      attrs?.fullwidth?.desktop?.value ??
      attrs?.fullwidth?.value ??
      (typeof attrs?.fullwidth === 'string' ? attrs.fullwidth : undefined)
    );
  }

  function getLayoutValueFromCtx(ctx) {
    // Divi has used both an attrs context and the module attrs object itself.
    // Resolve all known wrappers so these existing visibility callbacks keep
    // working when the settings renderer changes its callback context shape.
    const candidates = [
      ctx?.attrs,
      ctx?.moduleAttrs,
      ctx?.attributes,
      ctx?.module?.attrs,
      ctx?.module?.attributes,
      ctx?.module?.props?.attrs,
      ctx?.props?.attrs,
      ctx?.value?.attrs,
      ctx
    ];
    for (let i = 0; i < candidates.length; i++) {
      const value = getLayoutValueFromAttrs(candidates[i]);
      if (value !== undefined && value !== null && value !== '') {
        return value;
      }
    }
    return 'off';
  }

  function isMasonryLayoutSelected(ctx) {
    return getLayoutValueFromCtx(ctx) === {$masonry};
  }

  function addMasonryExclusionToVisibility(field) {
    if (!field || typeof field !== 'object' || masonryGridVisibilityWrappedFields.has(field)) {
      return;
    }

    const previousVisible = field.visible;
    field.visible = function(ctx) {
      const previouslyVisible = typeof previousVisible === 'function'
        ? previousVisible.apply(this, arguments)
        : previousVisible !== false;

      return previouslyVisible && !isMasonryLayoutSelected(ctx);
    };

    const previousVisibleCallback = field.visibleCallback;
    field.visibleCallback = function(ctx) {
      const previouslyVisible = typeof previousVisibleCallback === 'function'
        ? previousVisibleCallback.apply(this, arguments)
        : previousVisibleCallback !== false;

      return previouslyVisible && !isMasonryLayoutSelected(ctx);
    };

    if (field.component?.props) {
      const previousComponentVisible = field.component.props.visible;
      field.component.props.visible = function(ctx) {
        const previouslyVisible = typeof previousComponentVisible === 'function'
          ? previousComponentVisible.apply(this, arguments)
          : previousComponentVisible !== false;

        return previouslyVisible && !isMasonryLayoutSelected(ctx);
      };
    }

    masonryGridVisibilityWrappedFields.add(field);
  }

  function isThumbnailOrientationCompositeField(field, key, group) {
    const groupAttrName = String(group?.attrName || '');
    const groupLabel = String(group?.groupLabel || group?.fieldLabel || '');
    const attrName = String(field?.attrName || field?.component?.props?.attrName || groupAttrName);
    const subName = String(field?.subName || field?.component?.props?.subName || field?.component?.props?.fieldName || '');
    const label = String(field?.label || field?.component?.props?.fieldLabel || field?.component?.props?.groupLabel || field?.component?.props?.label || groupLabel);
    const fieldKey = String(key || '');

    return (
      groupAttrName === 'image.advanced.orientation' ||
      groupAttrName === 'imageAdvancedOrientation' ||
      groupLabel === 'Thumbnail Orientation' ||
      attrName === 'image.advanced.orientation' ||
      attrName === 'imageAdvancedOrientation' ||
      (attrName === 'image.advanced' && subName === 'orientation') ||
      fieldKey === 'imageAdvancedOrientation' ||
      fieldKey === 'image-advanced-orientation' ||
      label === 'Thumbnail Orientation'
    );
  }

  function setMasonryFieldVisibility(attributeDefinition) {
    const items = attributeDefinition?.settings?.innerContent?.items;
    if (!items || typeof items !== 'object') {
      return;
    }
    Object.keys(items).forEach((key) => {
      const field = items[key];
      if (field && typeof field === 'object') {
        field.visible = isMasonryLayoutSelected;
        if (field.component?.props) {
          field.component.props.visible = isMasonryLayoutSelected;
        }
        delete field.showIf;
      }
    });
  }

  // Extend the layout field.
  hooks.addFilter('divi.moduleLibrary.moduleAttributes.divi.gallery', 'divi-booster-gallery-masonry-layout-fields', (attributes, metadata) => {
    try {
      attributes[{$columnCountAttr}] = {$columnCountAttrDefinition};
      attributes[{$horizontalGapAttr}] = {$horizontalGapAttrDefinition};
      attributes[{$verticalGapAttr}] = {$verticalGapAttrDefinition};
      setMasonryFieldVisibility(attributes[{$columnCountAttr}]);
      setMasonryFieldVisibility(attributes[{$horizontalGapAttr}]);
      setMasonryFieldVisibility(attributes[{$verticalGapAttr}]);

      const fullwidth =
        attributes?.layout?.settings?.advanced?.fullwidth ||
        attributes?.module?.settings?.advanced?.items?.fullwidth ||
        attributes?.module?.advanced?.fullwidth ||
        attributes?.module?.settings?.advanced?.fullwidth;

      // Handle grouped and direct field shapes.
      let optionsObj;
      if (fullwidth?.item?.component?.props?.options) {
        optionsObj = fullwidth.item.component.props.options;
      }

      // Add the option once.
      if (optionsObj && !optionsObj[{$masonry}]) {
        optionsObj[{$masonry}] = { label: {$masonryLabel} };
      }

      // Keep option order deterministic.
      if (optionsObj && typeof optionsObj === 'object') {
        const sorted = Object.keys(optionsObj)
          .sort((a, b) => String(optionsObj[a].label).localeCompare(String(optionsObj[b].label)))
          .reduce((o, k) => (o[k] = optionsObj[k], o), {});
        fullwidth.item.component.props.options = sorted;
      }

      // Describe the layout once.
      if (
        fullwidth?.item?.description !== undefined &&
        String(fullwidth.item.description).indexOf('Masonry') === -1
      ) {
        fullwidth.item.description =
          String(fullwidth.item.description || '').trim() + ' ' + {$descNote};
      }

      // Keep shared gallery controls available for supported layouts.
      function attachVisibleCallback(item, callback) {
        const targets = [];
        if (item && typeof item === 'object') {
          targets.push(item);
          if (item.item && typeof item.item === 'object' && item.item !== item) {
            targets.push(item.item);
          }
        }
        targets.forEach((target) => {
          const prevVisible = target.visible;
          target.visible = function(ctx) {
            return callback(ctx, prevVisible);
          };
          if (target?.component?.props) {
            const prevComponentVisible = target.component.props.visible;
            target.component.props.visible = function(ctx) {
              const prev = (typeof prevComponentVisible === 'function') ? prevComponentVisible(ctx) : true;
              return prev && callback(ctx, prevVisible);
            };
          }
        });
      }
      function addVisibleGuard(item) {
        attachVisibleCallback(item, function(ctx, prevVisible) {
          try {
            const v = getLayoutValueFromCtx(ctx);
            const showWhen = (v === 'off' || v === {$masonry});
            return (typeof prevVisible === 'function') ? (prevVisible(ctx) && showWhen) : showWhen;
          } catch(e) { return true; }
        });
      }
      function hideWhenMasonry(item) {
        attachVisibleCallback(item, function(ctx, prevVisible) {
          try {
            const notMasonry = getLayoutValueFromCtx(ctx) !== {$masonry};
            return (typeof prevVisible === 'function') ? (prevVisible(ctx) && notMasonry) : notMasonry;
          } catch(e) { return true; }
        });
      }
      function hideFieldsWhenMasonry(root, matcher) {
        const seen = new Set();
        function visit(node) {
          if (!node || typeof node !== 'object' || seen.has(node)) {
            return;
          }
          seen.add(node);
          const field = node.item && typeof node.item === 'object' ? node.item : node;
          if (matcher(field, node)) {
            hideWhenMasonry(node);
          }
          Object.keys(node).forEach((key) => visit(node[key]));
        }
        visit(root);
      }
      // Find the settings collection.
      const items = attributes?.module?.settings?.innerContent?.items || attributes?.module?.settings?.advanced?.items || {};
      addVisibleGuard(items?.postsNumber || items?.posts_number);
      addVisibleGuard(items?.showPagination || items?.show_pagination);
      addVisibleGuard(items?.showTitleAndCaption || items?.show_title_and_caption);

      // Hide controls that do not apply to Masonry.
      hideWhenMasonry(items?.orientation);
      hideWhenMasonry(items?.layoutStyle || items?.layout_style || items?.galleryLayoutStyle || items?.gallery_layout_style);
      Object.keys(items || {}).forEach((key) => {
        const item = items[key];
        if (item?.item?.label === 'Layout Style' || item?.label === 'Layout Style') {
          hideWhenMasonry(item);
        }
      });
      hideFieldsWhenMasonry(attributes, function(field) {
        const attrName = String(field?.attrName || '');
        const subName = String(field?.subName || field?.component?.props?.subName || field?.component?.props?.fieldName || '');
        const label = String(field?.label || field?.component?.props?.fieldLabel || field?.component?.props?.groupLabel || field?.component?.props?.label || '');
        return (
          attrName === 'image.advanced.orientation' ||
          (attrName === 'image.advanced' && subName === 'orientation') ||
          attrName === 'layout.advanced.orientation' ||
          attrName === 'module.advanced.orientation' ||
          attrName === 'galleryGrid.decoration.layout.display' ||
          (attrName === 'galleryGrid.decoration.layout' && subName === 'display') ||
          attrName === 'image.advanced.layoutStyle' ||
          attrName === 'image.advanced.layout_style' ||
          attrName === 'layout.advanced.layoutStyle' ||
          attrName === 'layout.advanced.layout_style' ||
          attrName === 'module.advanced.layoutStyle' ||
          attrName === 'module.advanced.layout_style' ||
          label === 'Thumbnail Orientation' ||
          label === 'Layout Style'
        );
      });
    } catch(e) {}
    return attributes;
  });

  // Divi's Grid Builder fields are produced by the shared Layout option group,
  // rather than being nested in the Gallery module attribute metadata. Extend
  // that group at its own filter and preserve every existing visibility rule.
  hooks.addFilter('divi.module.options.layout.group.fields', 'divi-booster-gallery-masonry-hide-grid-layout-fields', (fields, group) => {
    if (!fields || typeof fields !== 'object' || group?.attrName !== 'galleryGrid.decoration.layout') {
      return fields;
    }

    Object.keys(fields).forEach((key) => {
      const field = fields[key];
      const fieldName = String(field?.subName || key);
      if (masonryGridLayoutFieldNames.has(key) || masonryGridLayoutFieldNames.has(fieldName)) {
        addMasonryExclusionToVisibility(field);
      }
    });

    return fields;
  }, 20);

  hooks.addFilter('divi.module.options.composite.group.fields', 'divi-booster-gallery-masonry-hide-thumbnail-orientation-field', (fields, group) => {
    if (!fields || typeof fields !== 'object') {
      return fields;
    }

    Object.keys(fields).forEach((key) => {
      const field = fields[key];
      if (field && typeof field === 'object' && isThumbnailOrientationCompositeField(field, key, group)) {
        addMasonryExclusionToVisibility(field);
      }
    });

    return fields;
  }, 20);

  hooks.addFilter('divi.moduleLibrary.moduleSettings.groups.divi.gallery', 'divi-booster-gallery-masonry-hide-groups-fields', (groups) => {
    function shouldShowOutsideMasonry(ctx, prevVisible) {
      try {
        const notMasonry = getLayoutValueFromCtx(ctx) !== {$masonry};
        return (typeof prevVisible === 'function') ? (prevVisible(ctx) && notMasonry) : notMasonry;
      } catch(e) {
        return true;
      }
    }
    function attachVisibility(node) {
      const targets = [];
      if (node && typeof node === 'object') {
        targets.push(node);
        if (node.item && typeof node.item === 'object' && node.item !== node) {
          targets.push(node.item);
        }
      }
      targets.forEach((target) => {
        const prevVisible = target.visible;
        target.visible = function(ctx) {
          return shouldShowOutsideMasonry(ctx, prevVisible);
        };
        const prevVisibleCallback = target.visibleCallback;
        target.visibleCallback = function(ctx) {
          return shouldShowOutsideMasonry(ctx, prevVisibleCallback);
        };
        if (target?.component?.props) {
          const prevComponentVisible = target.component.props.visible;
          target.component.props.visible = function(ctx) {
            return shouldShowOutsideMasonry(ctx, prevComponentVisible);
          };
        }
      });
    }
    function hideMatchingFields(root) {
      const seen = new Set();
      function visit(node) {
        if (!node || typeof node !== 'object' || seen.has(node)) {
          return;
        }
        seen.add(node);
        const field = node.item && typeof node.item === 'object' ? node.item : node;
        const attrName = String(field?.attrName || '');
        const subName = String(field?.subName || field?.component?.props?.subName || field?.component?.props?.fieldName || '');
        const label = String(field?.label || field?.component?.props?.fieldLabel || field?.component?.props?.groupLabel || '');
        if (
          attrName === 'image.advanced.orientation' ||
          (attrName === 'image.advanced' && subName === 'orientation') ||
          attrName === 'layout.advanced.orientation' ||
          attrName === 'module.advanced.orientation' ||
          attrName === 'galleryGrid.decoration.layout.display' ||
          (attrName === 'galleryGrid.decoration.layout' && subName === 'display') ||
          attrName === 'image.advanced.layoutStyle' ||
          attrName === 'image.advanced.layout_style' ||
          attrName === 'layout.advanced.layoutStyle' ||
          attrName === 'layout.advanced.layout_style' ||
          attrName === 'module.advanced.layoutStyle' ||
          attrName === 'module.advanced.layout_style' ||
          label === 'Thumbnail Orientation' ||
          label === 'Layout Style'
        ) {
          attachVisibility(node);
        }
        Object.keys(node).forEach((key) => visit(node[key]));
      }
      visit(root);
    }
    hideMatchingFields(groups);
    return groups;
  });

  function initBuiltInLayoutFieldVisibilityFallback() {
    if (window.dbgbMasonryBuiltInLayoutVisibilityFallback) {
      return;
    }
    window.dbgbMasonryBuiltInLayoutVisibilityFallback = true;

    const hiddenLabels = ['Thumbnail Orientation', 'Layout Style'];
    const hiddenFieldSelectors = [
      '.et-vb-field-image-advanced-orientation',
      '.et-vb-field-gallery-grid-decoration-layout-display',
      '[id="et-vb-image-advanced-orientation"]',
      '[id="et-vb-galleryGrid-decoration-layout--display"]',
      '[data-attr-name="image.advanced.orientation"]',
      '[data-attr-name="galleryGrid.decoration.layout.display"]',
      '[data-field-name="image-advanced-orientation"]',
      '[data-field-name="gallery-grid-decoration-layout-display"]'
    ];
    let scheduled = false;

    function textOf(el) {
      return String(el?.textContent || '').replace(/\s+/g, ' ').trim();
    }

    function getLayoutGroups() {
      return Array.prototype.slice.call(document.querySelectorAll('.et-vb-modal-group-title[data-name="designLayout"]'))
        .map((title) => title.closest('.et-vb-modal-group'))
        .filter(Boolean);
    }

    function isSelectedMasonryControl(el) {
      if (!el) {
        return false;
      }
      const value = String(el.value || el.getAttribute('value') || el.getAttribute('data-value') || '');
      const text = textOf(el);
      if (value === {$masonry}) {
        return true;
      }
      if (el.matches('option:checked') && (value === {$masonry} || text === {$masonryLabel})) {
        return true;
      }
      if ((el.getAttribute('aria-selected') === 'true' || el.getAttribute('aria-checked') === 'true' || el.checked === true) && (value === {$masonry} || text === {$masonryLabel})) {
        return true;
      }
      if (el.matches('button, [role="button"], [role="combobox"], [aria-haspopup="listbox"]') && text === {$masonryLabel}) {
        return true;
      }
      return false;
    }

    function groupHasMasonryLayout(group) {
      if (!group) {
        return false;
      }
      if (group.querySelector('select option:checked[value="' + {$masonry} + '"]')) {
        return true;
      }
      const selectedCandidates = group.querySelectorAll(
        'option:checked, input:checked, [aria-selected="true"], [aria-checked="true"], [data-value="' + {$masonry} + '"], [value="' + {$masonry} + '"], button, [role="button"], [role="combobox"], [aria-haspopup="listbox"]'
      );
      return Array.prototype.some.call(selectedCandidates, isSelectedMasonryControl);
    }

    function findSettingRow(labelEl) {
      const field = labelEl.closest('.et-vb-field');
      if (field) {
        return field;
      }
      return labelEl.closest('[data-attr-name], [data-field-name], [class*="field"], [class*="Field"], [class*="option"], [class*="Option"], [class*="control"], [class*="Control"]');
    }

    function restoreRows(activeRows) {
      document.querySelectorAll('[data-dbgb-masonry-built-in-layout-hidden="1"]').forEach((row) => {
        if (activeRows.has(row)) {
          return;
        }
        row.style.display = row.dataset.dbgbMasonryPreviousDisplay || '';
        delete row.dataset.dbgbMasonryBuiltInLayoutHidden;
        delete row.dataset.dbgbMasonryPreviousDisplay;
      });
    }

    function hideRow(row) {
      if (!row || row.dataset.dbgbMasonryBuiltInLayoutHidden === '1') {
        return;
      }
      row.dataset.dbgbMasonryBuiltInLayoutHidden = '1';
      row.dataset.dbgbMasonryPreviousDisplay = row.style.display || '';
      row.style.display = 'none';
    }

    function getRowsToHide(group) {
      const rows = new Set();
      hiddenFieldSelectors.forEach((selector) => {
        group.querySelectorAll(selector).forEach((el) => {
          const row = el.classList?.contains('et-vb-field') ? el : findSettingRow(el);
          if (row && row !== group && group.contains(row)) {
            rows.add(row);
          }
        });
      });
      group.querySelectorAll('label, span, div, button').forEach((el) => {
        if (hiddenLabels.indexOf(textOf(el)) === -1) {
          return;
        }
        const row = findSettingRow(el);
        if (row && row !== group && group.contains(row)) {
          rows.add(row);
        }
      });
      return rows;
    }

    function refreshVisibility() {
      scheduled = false;
      const activeRows = new Set();
      getLayoutGroups().forEach((group) => {
        if (!groupHasMasonryLayout(group)) {
          return;
        }
        getRowsToHide(group).forEach((row) => activeRows.add(row));
      });
      restoreRows(activeRows);
      activeRows.forEach(hideRow);
    }

    function scheduleRefresh() {
      if (scheduled) {
        return;
      }
      scheduled = true;
      window.requestAnimationFrame(refreshVisibility);
    }

    function attachWatchers() {
      const observerTarget = document.body || document.documentElement;
      if (!observerTarget || !observerTarget.nodeType) {
        window.setTimeout(attachWatchers, 50);
        return;
      }

      scheduleRefresh();
      document.addEventListener('click', scheduleRefresh, true);
      document.addEventListener('input', scheduleRefresh, true);
      document.addEventListener('change', scheduleRefresh, true);
      new MutationObserver(scheduleRefresh).observe(observerTarget, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['aria-selected', 'aria-checked', 'class', 'value']
      });
    }

    attachWatchers();
  }

  initBuiltInLayoutFieldVisibilityFallback();

  // Extend module conversion mappings.
  hooks.addFilter('divi.moduleLibrary.conversion.moduleConversionOutline', 'divi', (outline, name) => {
    if (name !== {$d4slug}) return outline;
    if (!outline.module) outline.module = {};
    // Preserve an existing mapping when present.
    if (!outline.module.fullwidth) outline.module.fullwidth = 'module.advanced.fullwidth.*';
    const columnCountConversionMap = {$columnCountConversionMap};
    Object.keys(columnCountConversionMap).forEach((key) => {
      outline.module[key] = columnCountConversionMap[key];
    });
    return outline;
  });

  hooks.addFilter('divi.module.classnames.value', 'divi-booster-gallery-masonry-preview-classnames', (classnames, param) => {
    const attrs = getAttrsFromParam(param);
    if (param?.name !== {$d5slug} || !isMasonryEnabled(attrs)) {
      return classnames;
    }

    return appendClassNames(classnames, getMasonryPreviewClasses(param, attrs, false).all);
  });

  hooks.addFilter('divi.module.wrapper.render', 'divi-booster-gallery-masonry-preview', (moduleWrapper, param) => {
    const moduleName = param?.name;
    if (moduleName !== {$d5slug} || !moduleWrapper?.props) {
      return moduleWrapper;
    }

    const attrs = getAttrsFromParam(param, moduleWrapper);
    if (!isMasonryEnabled(attrs)) {
      return moduleWrapper;
    }

    const columns = getMasonryColumns(attrs);
    const horizontalGaps = getMasonryHorizontalGaps(attrs);
    const verticalGaps = getMasonryVerticalGaps(attrs);
    const postsNumber = getMasonryPostsNumber(attrs);
    const imageOrder = getMasonryImageOrder(attrs);
    const galleryIds = getMasonryGalleryIds(attrs);
    const suppressSlideIn = shouldSuppressMasonryPreviewSlideIn(param, attrs);
    const classes = getMasonryPreviewClasses(param, attrs, suppressSlideIn);
    const previewStyles = window.vendor.wp.element.createElement(
      'style',
      { key: 'dbgb-masonry-vb-preview-styles' },
      getMasonryPreviewCss('.' + classes.scope, columns, horizontalGaps, verticalGaps)
    );
    const imageFixer = window.vendor.wp.element.createElement(
      MasonryPreviewImageFix,
      { key: 'dbgb-masonry-vb-preview-images', scope: classes.scope }
    );
    const trueMasonryLayout = window.vendor.wp.element.createElement(
      TrueMasonryPreviewLayout,
      { key: 'dbgb-masonry-vb-true-layout', scope: classes.scope, columns, horizontalGaps, verticalGaps, postsNumber, imageOrder, galleryIds, enabled: true }
    );
    const wrapperProps = Object.assign({}, moduleWrapper.props, {
      classname: appendClassNames(moduleWrapper.props.classname || '', classes.all),
      className: appendClassNames(moduleWrapper.props.className || '', classes.all)
    });

    return window.vendor.wp.element.cloneElement(
      moduleWrapper,
      wrapperProps,
      moduleWrapper.props.children,
      [previewStyles, imageFixer, trueMasonryLayout]
    );
  });

  function MasonryPreviewImageFix(props) {
    const useEffect = window.vendor?.wp?.element?.useEffect;
    if (typeof useEffect !== 'function') {
      return null;
    }

    useEffect(() => {
      const selector = '.' + props.scope;
      let frameId = 0;

      function normalizeImages() {
        const module = document.querySelector(selector);
        if (!module) {
          return;
        }
        module.querySelectorAll('.et_pb_gallery_image a[href] img').forEach((img) => {
          const anchor = img.closest('a[href]');
          const fullSrc = anchor ? anchor.getAttribute('href') : '';
          if (!isLikelyImageUrl(fullSrc)) {
            return;
          }

          if (img.getAttribute('src') !== fullSrc) {
            img.setAttribute('src', fullSrc);
          }
          img.removeAttribute('srcset');
          img.removeAttribute('sizes');
          img.removeAttribute('width');
          img.removeAttribute('height');
          img.dataset.diviboosterMasonryFullImage = '1';
        });
      }

      function scheduleNormalizeImages() {
        if (frameId) {
          return;
        }
        frameId = window.requestAnimationFrame(() => {
          frameId = 0;
          normalizeImages();
        });
      }

    normalizeImages();
    const observerTarget = document.querySelector(selector);
    const observer = observerTarget ? new MutationObserver(scheduleNormalizeImages) : null;
    if (observer) {
      observer.observe(observerTarget, { childList: true, subtree: true });
    }

    return () => {
      if (frameId) {
        window.cancelAnimationFrame(frameId);
      }
      if (observer) {
        observer.disconnect();
      }
    };
    }, [props.scope]);

    return null;
  }

  function TrueMasonryPreviewLayout(props) {
    const useEffect = window.vendor?.wp?.element?.useEffect;
    const useRef = window.vendor?.wp?.element?.useRef;
    if (typeof useEffect !== 'function' || typeof useRef !== 'function') {
      return null;
    }
    const latestPropsRef = useRef(props);
    const scheduleRef = useRef(null);

    useEffect(() => {
      const selector = '.' + props.scope;
      let frameId = 0;
      let imageListeners = [];
      let observer = null;
      let resizeObserver = null;
      let settleTimeout = 0;
      let settleAttempts = 0;
      let clickTimeouts = [];

      function getModule() {
        return document.querySelector(selector);
      }

      function hasGalleryItems(module) {
        return !!module?.querySelector?.('.et_pb_gallery_items .et_pb_gallery_item, .et_pb_gallery_items > .dbgb-masonry-column > .et_pb_gallery_item');
      }

      function stopObserver() {
        if (observer) {
          observer.disconnect();
          observer = null;
        }
      }

      function clearSettleTimer() {
        if (settleTimeout) {
          window.clearTimeout(settleTimeout);
          settleTimeout = 0;
        }
      }

      function scheduleSettleLayout() {
        if (!latestPropsRef.current.enabled || settleAttempts >= 5 || settleTimeout) {
          return;
        }
        settleAttempts++;
        settleTimeout = window.setTimeout(() => {
          settleTimeout = 0;
          schedule();
        }, settleAttempts * 120);
      }

      function clearImageListeners() {
        imageListeners.forEach((entry) => {
          entry.img.removeEventListener('load', entry.handler);
        });
        imageListeners = [];
      }

      function watchImages(module) {
        clearImageListeners();
        if (!module || !latestPropsRef.current.enabled) {
          return;
        }
        Array.prototype.slice.call(module.querySelectorAll('.et_pb_gallery_image img')).forEach((img) => {
          if (!img.complete) {
            const handler = () => schedule();
            img.addEventListener('load', handler, { once: true });
            imageListeners.push({ img, handler });
          }
        });
      }

      function forcePreviewFirstPage(module) {
        if (!module) {
          return;
        }

        module.setAttribute('data-dbgb-masonry-page', '1');

        const pagination = module.querySelector('.et_pb_gallery_pagination');
        if (pagination) {
          pagination.querySelectorAll('a.active').forEach((link) => link.classList.remove('active'));
          const firstLink = pagination.querySelector('a');
          if (firstLink) {
            firstLink.classList.add('active');
          }
        }
      }

      function forceAndSchedule(module) {
        forcePreviewFirstPage(module);
        schedule();
      }

      function scheduleAfterPreviewClick(event) {
        const module = getModule();
        if (!module || !module.contains(event.target)) {
          return;
        }
        if (event.target?.closest?.('.et_pb_gallery_pagination')) {
          event.preventDefault();
          event.stopPropagation();
          if (typeof event.stopImmediatePropagation === 'function') {
            event.stopImmediatePropagation();
          }
        }
        [0, 16, 50, 120, 250, 500].forEach((delay) => {
          clickTimeouts.push(window.setTimeout(() => forceAndSchedule(module), delay));
        });
      }

      function run() {
        frameId = 0;
        const module = getModule();
        if (!module) {
          return;
        }
        const latestProps = latestPropsRef.current;
        if (latestProps.enabled) {
          if (!hasGalleryItems(module)) {
            return;
          }
          forcePreviewFirstPage(module);
          layoutTrueMasonryModule(module, getResponsiveColumnCount(latestProps.columns), getResponsiveGap(latestProps.horizontalGaps), getResponsiveGap(latestProps.verticalGaps), latestProps.postsNumber, latestProps.imageOrder, latestProps.galleryIds);
          watchImages(module);
          scheduleSettleLayout();
        } else {
          stopObserver();
          resetTrueMasonryModule(module);
          clearImageListeners();
          clearSettleTimer();
        }
      }

      function schedule() {
        if (frameId) {
          return;
        }
        frameId = window.requestAnimationFrame(run);
      }

      scheduleRef.current = schedule;
      schedule();
      const observerTarget = getModule();
      if (observerTarget) {
        observer = new MutationObserver(schedule);
        observer.observe(observerTarget, {
          childList: true,
          subtree: true,
          attributes: true,
          attributeFilter: ['class', 'style', 'data-per_page', 'src', 'srcset']
        });
        if (typeof window.ResizeObserver === 'function') {
          resizeObserver = new window.ResizeObserver(schedule);
          resizeObserver.observe(observerTarget);
        }
      }
      window.addEventListener('resize', schedule);
      document.addEventListener('click', scheduleAfterPreviewClick, true);

      return () => {
        scheduleRef.current = null;
        if (frameId) {
          window.cancelAnimationFrame(frameId);
        }
        stopObserver();
        if (resizeObserver) {
          resizeObserver.disconnect();
        }
        clearSettleTimer();
        clickTimeouts.forEach((timeout) => window.clearTimeout(timeout));
        clickTimeouts = [];
        window.removeEventListener('resize', schedule);
        document.removeEventListener('click', scheduleAfterPreviewClick, true);
        clearImageListeners();
        const module = getModule();
        if (module) {
          resetTrueMasonryModule(module);
        }
      };
    }, [props.scope]);

    useEffect(() => {
      latestPropsRef.current = props;
      const schedule = scheduleRef.current;
      if (typeof schedule === 'function') {
        schedule();
      }
    }, [props.scope, props.enabled, JSON.stringify(props.columns), JSON.stringify(props.horizontalGaps), JSON.stringify(props.verticalGaps), props.postsNumber, props.imageOrder, props.galleryIds.join(',')]);

    return null;
  }

  function getResponsiveColumnCount(columns) {
    return getCurrentResponsiveValue(columns, 4);
  }

  function getResponsiveGap(gaps) {
    return getCurrentResponsiveValue(gaps || {}, '30px');
  }

  function getCurrentResponsiveValue(values, fallback) {
    const width = window.innerWidth || document.documentElement.clientWidth || 1024;
    const settings = masonryBreakpointConfig?.settings || {};
    const matchingBreakpoint = Object.keys(settings).find((breakpoint) => {
      const min = parseFloat(settings[breakpoint]?.minWidth?.value || '');
      const max = parseFloat(settings[breakpoint]?.maxWidth?.value || '');
      return (!Number.isFinite(min) || width >= min) && (!Number.isFinite(max) || width <= max);
    });
    if (matchingBreakpoint && values?.[matchingBreakpoint] !== undefined) {
      return values[matchingBreakpoint];
    }
    return values?.[masonryBreakpointConfig?.base || 'desktop'] ?? values?.desktop ?? fallback;
  }

  function getPreviewVisibleItems(items, postsNumber) {
    const count = sanitizePostsNumber(postsNumber);
    return count > 0 && count < items.length ? items.slice(0, count) : items;
  }

  function hidePreviewSourceItem(item) {
    if (!item) {
      return;
    }
    if (item.dataset.dbgbMasonrySourceHidden !== '1') {
      item.dataset.dbgbMasonrySourceHidden = '1';
      item.dataset.dbgbMasonryPreviousDisplay = item.style.getPropertyValue('display') || '';
      item.dataset.dbgbMasonryPreviousDisplayPriority = item.style.getPropertyPriority('display') || '';
    }
    if (item.style.getPropertyValue('display') !== 'none' || item.style.getPropertyPriority('display') !== 'important') {
      item.style.setProperty('display', 'none', 'important');
    }
  }

  function restorePreviewSourceItem(item) {
    if (!item || item.dataset.dbgbMasonrySourceHidden !== '1') {
      return;
    }
    const display = item.dataset.dbgbMasonryPreviousDisplay || '';
    const priority = item.dataset.dbgbMasonryPreviousDisplayPriority || '';
    if (display) {
      item.style.setProperty('display', display, priority);
    } else {
      item.style.removeProperty('display');
    }
    delete item.dataset.dbgbMasonrySourceHidden;
    delete item.dataset.dbgbMasonryPreviousDisplay;
    delete item.dataset.dbgbMasonryPreviousDisplayPriority;
  }

  function getPreviewItemId(item) {
    const img = item?.querySelector?.('img[class*="wp-image-"]');
    const match = img?.className?.match?.(/(?:^|\s)wp-image-(\d+)(?:\s|$)/);
    if (match) {
      return match[1];
    }
    const anchor = item?.querySelector?.('.et_pb_gallery_image a[href]');
    return anchor ? anchor.getAttribute('href') || '' : '';
  }

  function getPreviewItemTitle(item) {
    const anchor = item?.querySelector?.('.et_pb_gallery_image a[title]');
    if (anchor) {
      return anchor.getAttribute('title') || '';
    }
    const img = item?.querySelector?.('img[alt]');
    return img ? img.getAttribute('alt') || '' : '';
  }

  function getPreviewBaseOrder(module, items, galleryIds, imageOrder) {
    const identities = items.map((item, index) => getPreviewItemIdentity(item) || ('node:' + index));
    const membership = identities.slice().sort().join('|');
    const gallerySignature = (Array.isArray(galleryIds) ? galleryIds : []).map(String).join(',');
    const previous = masonryPreviewBaseOrderState.get(module);
    const defaultOrderChanged = String(imageOrder || 'default') === 'default'
      && previous?.gallerySignature !== gallerySignature;
    if (previous && previous.membership === membership && !defaultOrderChanged) {
      return previous.indices;
    }

    const galleryIndex = {};
    (Array.isArray(galleryIds) ? galleryIds : []).map(String).forEach((id, index) => {
      if (galleryIndex[id] == null) {
        galleryIndex[id] = index;
      }
    });
    const indices = {};
    items.forEach((item, domIndex) => {
      const identity = identities[domIndex];
      const id = String(getPreviewItemId(item) || '');
      indices[identity] = galleryIndex[id] != null ? galleryIndex[id] : domIndex;
    });
    masonryPreviewBaseOrderState.set(module, { membership, gallerySignature, indices });
    return indices;
  }

  function sortPreviewItems(module, items, imageOrder, galleryIds) {
    const order = String(imageOrder || 'default');
    const idOrder = Array.isArray(galleryIds) && galleryIds.length ? galleryIds.map(String) : [];
    const baseIndex = {};
    const persistentBaseIndex = getPreviewBaseOrder(module, items, galleryIds, imageOrder);

    idOrder.forEach((id, index) => {
      if (baseIndex[id] == null) {
        baseIndex[id] = index;
      }
    });

    const withMeta = items.map((item, domIndex) => {
      const id = String(getPreviewItemId(item) || '');
      const identity = getPreviewItemIdentity(item) || ('node:' + domIndex);
      const storedBaseIndex = parseInt(item.dataset.dbgbMasonryBaseIndex || '', 10);
      const resolvedBaseIndex = persistentBaseIndex[identity] != null
        ? persistentBaseIndex[identity]
        : (baseIndex[id] != null
          ? baseIndex[id]
          : (Number.isFinite(storedBaseIndex) ? storedBaseIndex : domIndex));
      if (!item.dataset.dbgbMasonryBaseIndex) {
        item.dataset.dbgbMasonryBaseIndex = String(resolvedBaseIndex);
      }
      return {
        item,
        id,
        title: getPreviewItemTitle(item).toLowerCase(),
        domIndex,
        baseIndex: resolvedBaseIndex
      };
    });

    withMeta.sort((a, b) => {
      if (order === 'dbdb_reverse') {
        return b.baseIndex - a.baseIndex || a.domIndex - b.domIndex;
      }
      if (order === 'dbdb_by_id') {
        return (parseInt(a.id, 10) || 0) - (parseInt(b.id, 10) || 0) || a.baseIndex - b.baseIndex;
      }
      if (order === 'dbdb_by_id_reverse') {
        return (parseInt(b.id, 10) || 0) - (parseInt(a.id, 10) || 0) || a.baseIndex - b.baseIndex;
      }
      if (order === 'dbdb_alphabetical' || order === 'dbdb_alphabetical_reverse') {
        if (a.title !== b.title) {
          const cmp = a.title < b.title ? -1 : 1;
          return order === 'dbdb_alphabetical_reverse' ? -cmp : cmp;
        }
        return a.baseIndex - b.baseIndex || a.domIndex - b.domIndex;
      }
      return a.baseIndex - b.baseIndex || a.domIndex - b.domIndex;
    });

    return withMeta.map((entry, index) => {
      entry.item.dataset.dbgbMasonryOriginalIndex = String(index + 1);
      return entry.item;
    });
  }

  function uniquePreviewItemsByNode(items) {
    const seen = new Set();
    return items.filter((item) => {
      if (!item || seen.has(item)) {
        return false;
      }
      seen.add(item);
      return true;
    });
  }

  function getPreviewItemIdentity(item) {
    const id = String(getPreviewItemId(item) || '');
    if (id) {
      return 'media:' + id;
    }
    const img = item?.querySelector?.('img');
    const src = String(img?.currentSrc || img?.getAttribute?.('src') || '');
    if (src) {
      return 'src:' + src;
    }
    const title = getPreviewItemTitle(item);
    return title ? 'title:' + title.toLowerCase() : '';
  }

  function uniquePreviewItemsByIdentity(items) {
    const seenNodes = new Set();
    const seenIdentities = new Set();
    const unique = [];
    items.forEach((item) => {
      if (!item || seenNodes.has(item)) {
        return;
      }
      seenNodes.add(item);
      const identity = getPreviewItemIdentity(item);
      if (identity && seenIdentities.has(identity)) {
        return;
      }
      if (identity) {
        seenIdentities.add(identity);
      }
      unique.push(item);
    });
    return unique;
  }

  function getPreviewItems(module, container, imageOrder, galleryIds) {
    const rawDirectItems = Array.prototype.slice.call(container.querySelectorAll(':scope > .et_pb_gallery_item'));
    const directItems = uniquePreviewItemsByIdentity(rawDirectItems);
    return sortPreviewItems(module, directItems, imageOrder, galleryIds);
  }

  function resetPreviewMasonryContainer(container, items) {
    container.classList.remove('dbgb-masonry-ready');
    delete container.dataset.dbgbMasonrySignature;
    delete container.dataset.dbgbMasonryInputSignature;
    Array.prototype.slice.call(container.querySelectorAll(':scope > .dbgb-masonry-column')).forEach((column) => column.remove());
    items.forEach(restorePreviewSourceItem);
  }

  function getPreviewMasonryItemOrder(item, fallback) {
    const order = parseInt(item?.dataset?.dbgbMasonryOriginalIndex || '', 10);
    return Number.isFinite(order) ? order : fallback;
  }

  function planBalancedPreviewMasonry(items, count, verticalGapPixels) {
    const columnHeights = Array.from({ length: count }, () => 0);
    const columns = Array.from({ length: count }, () => []);
    const measured = items.map((item, index) => ({
      item,
      index,
      order: getPreviewMasonryItemOrder(item, index + 1),
      height: getTrueMasonryItemHeight(item) || (index + 1)
    }));

    measured.sort((a, b) => b.height - a.height || a.order - b.order || a.index - b.index);

    measured.forEach((entry) => {
      let shortestIndex = 0;
      for (let i = 1; i < columnHeights.length; i++) {
        if (columnHeights[i] < columnHeights[shortestIndex]) {
          shortestIndex = i;
        }
      }
      const isNotFirstInColumn = columns[shortestIndex].length > 0;
      columns[shortestIndex].push(entry);
      columnHeights[shortestIndex] += entry.height + (isNotFirstInColumn ? verticalGapPixels : 0);
    });

    columns.forEach((column) => {
      column.sort((a, b) => a.order - b.order || a.index - b.index);
    });

    columns.sort((a, b) => {
      const aOrder = a.length ? a[0].order : Number.POSITIVE_INFINITY;
      const bOrder = b.length ? b[0].order : Number.POSITIVE_INFINITY;
      return aOrder - bOrder;
    });

    const signatureParts = [];
    columns.forEach((column, columnIndex) => {
      column.forEach((entry) => {
        signatureParts.push(entry.order + ':' + columnIndex + ':' + Math.round(entry.height));
      });
    });

    return {
      columns: columns.map((column) => column.map((entry) => entry.item)),
      signatureParts
    };
  }

  function layoutTrueMasonryModule(module, count, horizontalGap, verticalGap, postsNumber, imageOrder, galleryIds) {
    const container = module.querySelector('.et_pb_gallery_items');
    if (!container) {
      return;
    }
    container.setAttribute('role', 'list');
    const items = getPreviewItems(module, container, imageOrder, galleryIds);
    if (!items.length) {
      return;
    }
    items.forEach((item, index) => {
      item.setAttribute('role', 'listitem');
      item.setAttribute('aria-posinset', String(index + 1));
      item.setAttribute('aria-setsize', String(items.length));
    });
    items.forEach((item, index) => {
      if (!item.dataset.dbgbMasonryOriginalIndex) {
        item.dataset.dbgbMasonryOriginalIndex = String(index + 1);
      }
    });
    items.sort((a, b) => parseInt(a.dataset.dbgbMasonryOriginalIndex || '0', 10) - parseInt(b.dataset.dbgbMasonryOriginalIndex || '0', 10));
    count = Math.max(1, Math.min(10, parseInt(count, 10) || 1));
    horizontalGap = sanitizeGap(horizontalGap, '30px');
    verticalGap = sanitizeGap(verticalGap, '30px');
    const visibleItems = getPreviewVisibleItems(items, postsNumber);
    const verticalGapPixels = gapToPixels(verticalGap, container);
    container.style.gap = horizontalGap;

    const inputSignature = [
      'columns:' + count,
      'hgap:' + horizontalGap,
      'vgap:' + verticalGap,
      'posts:' + postsNumber,
      'order:' + imageOrder,
      'ids:' + (Array.isArray(galleryIds) ? galleryIds.join(',') : ''),
      'total:' + items.length,
      'sources:' + items.map((item) => {
        const img = item.querySelector?.('img');
        return getPreviewItemIdentity(item) + ':' + String(img?.currentSrc || img?.getAttribute?.('src') || '');
      }).join(';')
    ].concat(visibleItems.map((item) => item.dataset.dbgbMasonryOriginalIndex || '')).join('|');

    if (container.classList.contains('dbgb-masonry-ready') && container.dataset.dbgbMasonryInputSignature === inputSignature) {
      items.forEach(hidePreviewSourceItem);
      return;
    }

    resetPreviewMasonryContainer(container, items);

    const signatureParts = ['columns:' + count, 'hgap:' + horizontalGap, 'vgap:' + verticalGap, 'posts:' + postsNumber, 'order:' + imageOrder, 'ids:' + (Array.isArray(galleryIds) ? galleryIds.join(',') : ''), 'total:' + items.length];
    const planResult = planBalancedPreviewMasonry(visibleItems, count, verticalGapPixels);
    const plan = planResult.columns;
    signatureParts.push(...planResult.signatureParts);

    const signature = signatureParts.join('|');

    const columns = [];
    for (let i = 0; i < count; i++) {
      const column = document.createElement('div');
      column.className = 'dbgb-masonry-column';
      column.setAttribute('role', 'presentation');
      column.style.gap = verticalGap;
      container.appendChild(column);
      columns.push(column);
    }
    plan.forEach((columnItems, columnIndex) => {
      columnItems.forEach((item) => {
        const clone = item.cloneNode(true);
        delete clone.dataset.dbgbMasonrySourceHidden;
        delete clone.dataset.dbgbMasonryPreviousDisplay;
        delete clone.dataset.dbgbMasonryPreviousDisplayPriority;
        clone.dataset.dbgbMasonryPreviewClone = '1';
        clone.querySelectorAll('[id]').forEach((element) => element.removeAttribute('id'));
        clone.removeAttribute('id');
        columns[columnIndex].appendChild(clone);
      });
    });
    items.forEach(hidePreviewSourceItem);
    container.dataset.dbgbMasonrySignature = signature;
    container.dataset.dbgbMasonryInputSignature = inputSignature;
    container.classList.add('dbgb-masonry-ready');
  }

  function resetTrueMasonryModule(module) {
    const container = module?.querySelector?.('.et_pb_gallery_items');
    if (!container) {
      return;
    }
    const items = Array.prototype.slice.call(container.querySelectorAll(':scope > .et_pb_gallery_item'));
    container.classList.remove('dbgb-masonry-ready');
    delete container.dataset.dbgbMasonrySignature;
    delete container.dataset.dbgbMasonryInputSignature;
    Array.prototype.slice.call(container.querySelectorAll(':scope > .dbgb-masonry-column')).forEach((column) => column.remove());
    items.forEach(restorePreviewSourceItem);
  }

  function getTrueMasonryItemHeight(item) {
    const rect = item.getBoundingClientRect ? item.getBoundingClientRect() : null;
    if (rect?.height) {
      return rect.height;
    }
    if (item.offsetHeight) {
      return item.offsetHeight;
    }
    const img = item.querySelector?.('img');
    if (img?.naturalWidth && img?.naturalHeight) {
      return (img.naturalHeight / img.naturalWidth) * (item.offsetWidth || 1);
    }
    return 0;
  }

  function isLikelyImageUrl(url) {
    return /\.(?:jpe?g|png|gif|webp|avif|bmp|svg)(?:[?#].*)?$/i.test(String(url || ''));
  }

  function getAttrsFromParam(param, moduleWrapper) {
    const candidates = [
      param?.attrs ||
      {},
      param?.moduleAttrs ||
      {},
      param?.module?.attrs ||
      {},
      param?.module?.props?.attrs ||
      {},
      param?.module?.props?.moduleAttrs ||
      {},
      moduleWrapper?.props?.attrs ||
      {},
      moduleWrapper?.props?.moduleAttrs ||
      {}
    ];

    return candidates.reduce((attrs, candidate) => {
      return candidate && typeof candidate === 'object' ? Object.assign(attrs, candidate) : attrs;
    }, {});
  }

  function getMasonryPreviewClasses(param, attrs, suppressSlideIn) {
    const id = String(param?.id || param?.moduleOrderIndex || 'gallery')
      .replace(/[^a-zA-Z0-9_-]/g, '-')
      .replace(/^-+|-+$/g, '') || 'gallery';
    const scope = 'divibooster-gallery-masonry-vb-' + id;
    const slideInClass = isGridSlideInEnabled(attrs) && !suppressSlideIn ? '' : ' ' + {$gridSlideInOffClass};
    return {
      scope,
      all: 'divibooster-gallery-masonry divibooster-gallery-masonry-vb divibooster-gallery-true-masonry ' + scope + slideInClass
    };
  }

  function getMasonryPreviewStateKey(param) {
    return String(param?.id || param?.moduleOrderIndex || 'gallery');
  }

  function shouldSuppressMasonryPreviewSlideIn(param, attrs) {
    const key = getMasonryPreviewStateKey(param);
    const slideInEnabled = isGridSlideInEnabled(attrs);
    const previous = masonryPreviewSlideInState.get(key);
    masonryPreviewSlideInState.set(key, { slideInEnabled });

    return slideInEnabled && previous && previous.slideInEnabled === slideInEnabled;
  }

  function appendClassNames(classnames, extra) {
    const existing = String(classnames || '').trim();
    const extraClasses = String(extra || '').trim();
    if (!extraClasses) {
      return classnames;
    }
    if (!existing) {
      return extraClasses;
    }
    const all = existing.split(/\s+/);
    extraClasses.split(/\s+/).forEach((className) => {
      if (className && !all.includes(className)) {
        all.push(className);
      }
    });
    return all.join(' ');
  }

  function isMasonryEnabled(attrs) {
    return getGalleryLayoutValue(attrs) === {$masonry};
  }

  function isGridSlideInEnabled(attrs) {
    const legacyValue = attrs?.[{$gridSlideInD4Field}];
    if (legacyValue === 'off') {
      return false;
    }

    const attr = attrs?.[{$gridSlideInAttr}];
    if (attr?.desktop?.value === 'off' || attr?.value === 'off' || attr === 'off') {
      return false;
    }

    return true;
  }

  function getGalleryLayoutValue(attrs) {
    return (
      attrs?.layout?.advanced?.fullwidth?.desktop?.value ??
      attrs?.layout?.advanced?.fullwidth?.value ??
      attrs?.module?.advanced?.fullwidth?.desktop?.value ??
      attrs?.module?.advanced?.fullwidth?.value ??
      attrs?.fullwidth?.desktop?.value ??
      attrs?.fullwidth?.value ??
      attrs?.fullwidth ??
      ''
    );
  }

  function getMasonryColumns(attrs) {
    const customValues = getResponsiveAttrValues(attrs, {$columnCountAttr});
    const nativeValues = Object.keys(customValues).length ? {} : getNativeGridValues(attrs, 'gridColumnCount');
    const sourceValues = Object.assign(
      { desktop: 4, tablet: 2, phone: 1 },
      nativeValues,
      customValues
    );
    return resolveResponsiveValues(sourceValues, 4, sanitizeColumnCount);
  }

  function getMasonryPostsNumber(attrs) {
    return sanitizePostsNumber(
      attrs?.module?.advanced?.postsNumber?.desktop?.value ??
      attrs?.module?.advanced?.postsNumber?.value ??
      attrs?.postsNumber?.desktop?.value ??
      attrs?.postsNumber?.value ??
      attrs?.postsNumber ??
      attrs?.posts_number ??
      4
    );
  }

  function getMasonryImageOrder(attrs) {
    return String(
      attrs?.image?.advanced?.galleryOrderby?.desktop?.value ??
      attrs?.image?.advanced?.galleryOrderby?.value ??
      attrs?.image?.advanced?.galleryOrderby ??
      attrs?.gallery_orderby ??
      'default'
    );
  }

  function getMasonryGalleryIds(attrs) {
    const raw = (
      attrs?.image?.advanced?.galleryIds?.desktop?.value ??
      attrs?.image?.advanced?.galleryIds?.value ??
      attrs?.image?.advanced?.galleryIds ??
      attrs?.gallery_ids ??
      []
    );

    if (Array.isArray(raw)) {
      return raw.map((id) => String(id)).filter(Boolean);
    }
    if (typeof raw === 'string') {
      return raw.split(',').map((id) => id.trim()).filter(Boolean);
    }
    return [];
  }

  function getMasonryHorizontalGaps(attrs) {
    return getMasonryGapValues(attrs, {$horizontalGapAttr}, 'columnGap');
  }

  function getMasonryVerticalGaps(attrs) {
    return getMasonryGapValues(attrs, {$verticalGapAttr}, 'rowGap');
  }

  function getMasonryGapValues(attrs, attrName, nativeField) {
    let sourceValues = getResponsiveAttrValues(attrs, attrName);
    if (!Object.keys(sourceValues).length) {
      sourceValues = getNativeGridValues(attrs, nativeField);
    }
    if (!Object.keys(sourceValues).length) {
      sourceValues = { desktop: '30px', tablet: '30px', phone: '30px' };
    }
    return resolveResponsiveValues(sourceValues, '30px', sanitizeGap);
  }

  function getNativeGridValues(attrs, field) {
    const layout = attrs?.galleryGrid?.decoration?.layout || {};
    const values = {};
    Object.keys(layout).forEach((breakpoint) => {
      const value = layout?.[breakpoint]?.value?.[field] ?? layout?.[breakpoint]?.[field];
      if (value !== undefined) {
        values[breakpoint] = value;
      }
    });
    if (layout?.value?.[field] !== undefined || layout?.[field] !== undefined) {
      values.desktop = layout?.value?.[field] ?? layout?.[field];
    }
    return values;
  }

  function getResponsiveAttrValues(attrs, attrName) {
    const attr = attrs?.[attrName] || {};
    const values = {};
    Object.keys(attr).forEach((breakpoint) => {
      const value = attr?.[breakpoint]?.value ?? (typeof attr?.[breakpoint] !== 'object' ? attr?.[breakpoint] : undefined);
      if (value !== undefined && typeof value !== 'object') {
        values[breakpoint] = value;
      }
    });
    if (attr?.value !== undefined && typeof attr.value !== 'object') {
      values.desktop = attr.value;
    }
    return values;
  }

  function resolveResponsiveValues(sourceValues, defaultValue, sanitizer) {
    const settings = masonryBreakpointConfig?.settings || {};
    const base = settings?.[masonryBreakpointConfig?.base] ? masonryBreakpointConfig.base : 'desktop';
    const baseOrder = Number(settings?.[base]?.order ?? 50);
    const baseValue = sourceValues?.[base] ?? defaultValue;
    const resolved = {};

    Object.keys(settings).forEach((breakpoint) => {
      const targetOrder = Number(settings?.[breakpoint]?.order ?? baseOrder);
      let rawValue = baseValue;
      let bestDistance = Math.abs(targetOrder - baseOrder);
      Object.keys(sourceValues || {}).forEach((sourceBreakpoint) => {
        if (settings?.[sourceBreakpoint]?.order === undefined) {
          return;
        }
        const sourceOrder = Number(settings[sourceBreakpoint].order);
        const withinPath = targetOrder <= baseOrder
          ? sourceOrder >= targetOrder && sourceOrder <= baseOrder
          : sourceOrder <= targetOrder && sourceOrder >= baseOrder;
        const distance = Math.abs(targetOrder - sourceOrder);
        if (withinPath && distance <= bestDistance) {
          rawValue = sourceValues[sourceBreakpoint];
          bestDistance = distance;
        }
      });
      resolved[breakpoint] = sanitizer(rawValue, defaultValue);
    });

    if (!Object.keys(resolved).length) {
      resolved.desktop = sanitizer(baseValue, defaultValue);
    }
    return resolved;
  }

  function sanitizeColumnCount(value, fallback) {
    const parsed = parseInt(value, 10);
    if (!Number.isFinite(parsed)) {
      return fallback;
    }
    return Math.max(1, Math.min(10, parsed));
  }

  function sanitizePostsNumber(value) {
    const parsed = parseInt(value, 10);
    if (!Number.isFinite(parsed) || parsed <= 0) {
      return 4;
    }
    return parsed;
  }

  function sanitizeGap(value, fallback) {
    value = String(value == null ? '' : value).trim();
    fallback = String(fallback == null || fallback === '' ? '30px' : fallback).trim();
    if (!value) {
      return fallback;
    }
    if (/^\d+(?:\.\d+)?$/.test(value)) {
      value += 'px';
    }
    return /^(?:0|(?:\d+(?:\.\d+)?))(?:px|em|rem|%|vw|vh|vmin|vmax)?$/i.test(value) ? value : fallback;
  }

  function gapToPixels(value, context) {
    value = sanitizeGap(value, '0px');
    if (/px$/i.test(value)) {
      return parseFloat(value) || 0;
    }
    if (!context?.appendChild) {
      return 0;
    }
    const probe = document.createElement('div');
    probe.style.position = 'absolute';
    probe.style.visibility = 'hidden';
    probe.style.pointerEvents = 'none';
    probe.style.width = '0';
    probe.style.height = value;
    context.appendChild(probe);
    const pixels = probe.getBoundingClientRect?.().height || probe.offsetHeight || 0;
    probe.remove();
    return Number.isFinite(pixels) ? pixels : 0;
  }

  function getMasonryPreviewCss(scope, columns, horizontalGaps, verticalGaps) {
    const desktopGap = sanitizeGap(horizontalGaps?.desktop, '30px');
    const tabletGap = sanitizeGap(horizontalGaps?.tablet, desktopGap);
    const phoneGap = sanitizeGap(horizontalGaps?.phone, tabletGap);
    const desktopVerticalGap = sanitizeGap(verticalGaps?.desktop, '30px');
    const tabletVerticalGap = sanitizeGap(verticalGaps?.tablet, desktopVerticalGap);
    const phoneVerticalGap = sanitizeGap(verticalGaps?.phone, tabletVerticalGap);
    const responsiveCss = getMasonryPreviewResponsiveCss(scope, horizontalGaps, verticalGaps);
    return scope + ` .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_image,
` + scope + ` .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_title,
` + scope + ` .et_pb_gallery .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_image,
` + scope + ` .et_pb_gallery .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_title {
  max-width: 100% !important;
  width: 100% !important;
  height: auto !important;
  min-height: 0 !important;
  max-height: none !important;
  aspect-ratio: auto !important;
}
` + scope + ` .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_image,
` + scope + ` .et_pb_gallery .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_image {
  padding-top: 0 !important;
  display: block !important;
  position: relative !important;
}
` + scope + ` .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_image a,
` + scope + ` .et_pb_gallery .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_image a {
  display: block !important;
  width: 100% !important;
  height: auto !important;
  min-height: 0 !important;
  max-height: none !important;
  aspect-ratio: auto !important;
}
` + scope + ` .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_image img,
` + scope + ` .et_pb_gallery .et_pb_gallery_items .et_pb_gallery_item .et_pb_gallery_image img {
  width: 100% !important;
  height: auto !important;
  min-height: 0 !important;
  max-height: none !important;
  aspect-ratio: auto !important;
  object-fit: contain !important;
  position: static !important;
  display: block !important;
}
` + scope + ` .et_pb_gallery_items {
  list-style: none;
  margin-left: 0;
  padding-left: 0;
}
` + scope + ` .et_pb_gallery_pagination {
  visibility: hidden !important;
  pointer-events: none !important;
  min-height: 1em !important;
  margin-top: ` + desktopVerticalGap + ` !important;
}
` + scope + `.` + {$gridSlideInOffClass} + ` .et_pb_gallery_item,
` + scope + `.divibooster-gallery-true-masonry.` + {$gridSlideInOffClass} + ` .et_pb_gallery_item {
  -webkit-animation: none !important;
  -moz-animation: none !important;
  -o-animation: none !important;
  -ms-animation: none !important;
  animation: none !important;
}
` + scope + `.et-animated--vb,
` + scope + ` .et-animated--vb {
  animation: none !important;
  transition: none !important;
  opacity: 1 !important;
}
` + scope + `.divibooster-gallery-true-masonry .et_pb_gallery_items.dbgb-masonry-ready {
  display: flex !important;
  column-count: unset !important;
  column-gap: 0 !important;
  gap: ` + desktopGap + ` !important;
  align-items: flex-start !important;
}
` + scope + `.divibooster-gallery-true-masonry .et_pb_gallery_items.dbgb-masonry-ready::after {
  content: none !important;
  display: none !important;
}
@media (max-width: 1200px) {
  ` + scope + `.divibooster-gallery-true-masonry .et_pb_gallery_items.dbgb-masonry-ready {
    gap: ` + tabletGap + ` !important;
  }
}
@media (max-width: 767px) {
  ` + scope + `.divibooster-gallery-true-masonry .et_pb_gallery_items.dbgb-masonry-ready {
    gap: ` + phoneGap + ` !important;
  }
}
` + scope + `.divibooster-gallery-true-masonry .et_pb_gallery_items > .dbgb-masonry-column {
  break-inside: auto !important;
  page-break-inside: auto !important;
  -webkit-column-break-inside: auto !important;
  -moz-column-break-inside: auto !important;
  display: flex !important;
  flex: 1 1 0 !important;
  min-width: 0 !important;
  width: auto !important;
  max-width: none !important;
  margin: 0 !important;
  float: none !important;
  flex-direction: column;
  gap: ` + desktopVerticalGap + `;
}
` + scope + `.divibooster-gallery-true-masonry .dbgb-masonry-column {
  display: flex !important;
  flex: 1 1 0 !important;
  min-width: 0 !important;
  width: auto !important;
  max-width: none !important;
  flex-direction: column;
  gap: ` + desktopVerticalGap + `;
}
@media (max-width: 1200px) {
  ` + scope + ` .et_pb_gallery_pagination {
    margin-top: ` + tabletVerticalGap + ` !important;
  }
  ` + scope + `.divibooster-gallery-true-masonry .et_pb_gallery_items > .dbgb-masonry-column,
  ` + scope + `.divibooster-gallery-true-masonry .dbgb-masonry-column {
    gap: ` + tabletVerticalGap + `;
  }
}
@media (max-width: 767px) {
  ` + scope + ` .et_pb_gallery_pagination {
    margin-top: ` + phoneVerticalGap + ` !important;
  }
  ` + scope + `.divibooster-gallery-true-masonry .et_pb_gallery_items > .dbgb-masonry-column,
  ` + scope + `.divibooster-gallery-true-masonry .dbgb-masonry-column {
    gap: ` + phoneVerticalGap + `;
  }
}
` + scope + `.divibooster-gallery-true-masonry .dbgb-masonry-column .et_pb_gallery_item {
  display: block !important;
  width: 100% !important;
  max-width: 100% !important;
  margin: 0 !important;
}
` + responsiveCss;
  }

  function getMasonryPreviewResponsiveCss(scope, horizontalGaps, verticalGaps) {
    const settings = masonryBreakpointConfig?.settings || {};
    const base = masonryBreakpointConfig?.base || 'desktop';
    let css = '';

    Object.keys(settings).forEach((breakpoint) => {
      if (breakpoint === base) {
        return;
      }
      const min = settings?.[breakpoint]?.minWidth?.value;
      const max = settings?.[breakpoint]?.maxWidth?.value;
      const conditions = [];
      if (min) conditions.push('(min-width: ' + min + ')');
      if (max) conditions.push('(max-width: ' + max + ')');
      if (!conditions.length) {
        return;
      }
      const horizontalGap = sanitizeGap(horizontalGaps?.[breakpoint], '30px');
      const verticalGap = sanitizeGap(verticalGaps?.[breakpoint], '30px');
      css += '@media only screen and ' + conditions.join(' and ') + ' {'
        + scope + '.divibooster-gallery-true-masonry .et_pb_gallery_items.dbgb-masonry-ready{gap:' + horizontalGap + ' !important;}'
        + scope + ' .et_pb_gallery_pagination{margin-top:' + verticalGap + ' !important;}'
        + scope + '.divibooster-gallery-true-masonry .et_pb_gallery_items > .dbgb-masonry-column,'
        + scope + '.divibooster-gallery-true-masonry .dbgb-masonry-column{gap:' + verticalGap + ';}'
        + '}';
    });

    return css;
  }
})();
JS;
}

// === Divi 5 conversion mapping ===
add_filter('divi.moduleLibrary.conversion.moduleConversionOutline', __NAMESPACE__ . '\\divi5_conversion_outline_masonry', 10, 2);
function divi5_conversion_outline_masonry($conversion_outline, $module_name) {
    if ($module_name !== DIVI5_MODULE_SLUG) {
        return $conversion_outline;
    }
    if (!is_array($conversion_outline)) {
        return $conversion_outline;
    }
    if (!isset($conversion_outline['module']) || !is_array($conversion_outline['module'])) {
        $conversion_outline['module'] = array();
    }
    if (empty($conversion_outline['module']['fullwidth'])) {
        $conversion_outline['module']['fullwidth'] = 'module.advanced.fullwidth.*';
    }
    $conversion_outline['module'][DIVI4_COLUMN_COUNT_FIELD]          = DIVI5_COLUMN_COUNT_ATTR . '.*';
    $conversion_outline['module'][DIVI4_COLUMN_COUNT_TABLET_FIELD]   = DIVI5_COLUMN_COUNT_ATTR . '.*';
    $conversion_outline['module'][DIVI4_COLUMN_COUNT_PHONE_FIELD]    = DIVI5_COLUMN_COUNT_ATTR . '.*';
    $conversion_outline['module'][DIVI4_HORIZONTAL_GAP_FIELD]        = DIVI5_HORIZONTAL_GAP_ATTR . '.*';
    $conversion_outline['module'][DIVI4_HORIZONTAL_GAP_TABLET_FIELD] = DIVI5_HORIZONTAL_GAP_ATTR . '.*';
    $conversion_outline['module'][DIVI4_HORIZONTAL_GAP_PHONE_FIELD]  = DIVI5_HORIZONTAL_GAP_ATTR . '.*';
    $conversion_outline['module'][DIVI4_VERTICAL_GAP_FIELD]          = DIVI5_VERTICAL_GAP_ATTR . '.*';
    $conversion_outline['module'][DIVI4_VERTICAL_GAP_TABLET_FIELD]   = DIVI5_VERTICAL_GAP_ATTR . '.*';
    $conversion_outline['module'][DIVI4_VERTICAL_GAP_PHONE_FIELD]    = DIVI5_VERTICAL_GAP_ATTR . '.*';
    return $conversion_outline;
}


