<?php
/**
 * Drawers trait
 *
 * @package     AffiliateWP
 * @subpackage  Utils\Traits
 * @copyright   Copyright (c) 2025, Sandhills Development, LLC
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       2.33.0
 */

namespace AffiliateWP\Utils\Traits;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Drawers trait
 *
 * Provides reusable slide-over drawer rendering functionality.
 * Renders a right-anchored drawer panel with backdrop overlay,
 * Alpine.js state management, and customizable content sections.
 *
 * @since 2.33.0
 */
trait Drawers {

	/**
	 * Drawer width options mapped to Tailwind classes.
	 *
	 * @since 2.33.0
	 * @var array
	 */
	protected $drawer_widths = [
		'md'  => 'max-w-md',   // 448px
		'lg'  => 'max-w-lg',   // 512px
		'xl'  => 'max-w-xl',   // 576px
		'2xl' => 'max-w-2xl',  // 672px
		'3xl' => 'max-w-3xl',  // 768px
		'4xl' => 'max-w-4xl',  // 896px
		'5xl' => 'max-w-5xl',  // 1024px — matches the Pay Affiliates preview content column.
	];

	/**
	 * Drawer header style presets.
	 *
	 * @since 2.33.0
	 * @var array
	 */
	protected $drawer_header_styles = [
		'gradient' => [
			'bg'             => 'bg-affwp-brand-500',
			'title_color'    => 'text-white',
			'subtitle_color' => 'text-white/70',
			'close_bg'       => 'bg-affwp-brand-500 hover:bg-affwp-brand-600',
			'close_color'    => 'text-white/60 hover:text-white',
			'close_ring'     => 'focus-visible:ring-white focus-visible:ring-offset-affwp-brand-600',
		],
		'flat'     => [
			'bg'             => 'bg-white border-b border-gray-200',
			'title_color'    => 'text-gray-900',
			'subtitle_color' => 'text-gray-500',
			'close_bg'       => 'bg-white hover:bg-gray-50',
			'close_color'    => 'text-gray-400 hover:text-gray-500',
			'close_ring'     => 'focus-visible:ring-affwp-brand-500 focus-visible:ring-offset-white',
		],
	];

	/**
	 * Render a drawer component.
	 *
	 * Outputs the drawer shell markup including backdrop, slide-in panel,
	 * header, scrollable content area, optional summary section, and footer.
	 * Content is injected via PHP callbacks for maximum flexibility.
	 *
	 * The drawer expects an Alpine.js store conforming to this contract.
	 *
	 * Required store properties / methods:
	 * - isOpen (bool): Controls drawer visibility.
	 * - title (string): Drawer header title.
	 * - close(reason?): Closes the drawer. Optional `reason` arg distinguishes
	 *   X-button clicks ('x-button') from Escape keypress (no arg). Stores that
	 *   don't need to differentiate can ignore the positional arg.
	 *
	 * Optional store properties / methods:
	 * - isLoading (bool): Shows loading spinner on default footer save button.
	 * - isNew (bool): true for create mode, false for edit (default footer label).
	 * - subtitle (string): Drawer header subtitle.
	 * - errors (object): Validation errors keyed by field name. `errors.general`
	 *   shows in the top error banner.
	 * - drawerWidth (string): Dynamic Tailwind max-width class, e.g. 'max-w-2xl'.
	 *   Overrides the $args['width'] default at runtime.
	 * - hideHeaderClose (bool): Hide the drawer-shell close X. Use for drawers
	 *   that take over their body for a focused sub-task (template picker,
	 *   preview maximize) where the drawer-level close competes with the
	 *   sub-task's own back/cancel.
	 * - contentClass (string): Override the body padding/spacing classes.
	 *   Default 'px-6 py-6 space-y-6'. Stores that take over the body for
	 *   full-bleed sub-tasks can set this to '' or a custom value.
	 * - save() (when using the default footer): Triggers the save action.
	 *
	 * Dirty-check additions (only when $args['dirty_check'] = true):
	 * - confirmClose(reason?): Called in place of close() on Escape, backdrop
	 *   (disabled by default when dirty_check is on), X-button, and Cancel.
	 * - _markDirty(): Called automatically on @input/@change inside the body.
	 *
	 * Editable-title additions (only when $args['editable_title'] = true):
	 * - isEditingTitle (bool): View vs. edit mode flag.
	 * - titleDraft (string): Buffer for the inline input.
	 * - startEditTitle(): Swap h2 → input.
	 * - commitTitle(): Save the draft + return to view mode.
	 * - cancelTitle(): Discard draft + return to view mode.
	 *
	 * @since 2.33.0
	 *
	 * @param array $args {
	 *     Drawer arguments.
	 *
	 *     @type string   $id                       Unique ID for the drawer. Required.
	 *     @type string   $alpine_store             Alpine.js store name. Required.
	 *     @type string   $width                    Drawer width (md, lg, xl, 2xl). Default 'xl'.
	 *     @type string   $header_style             Header style preset (gradient, flat). Default 'gradient'.
	 *     @type callable $content_callback         PHP callable for body content. Optional.
	 *     @type array    $content_args             Arguments passed to content callback. Default [].
	 *     @type callable $summary_callback         PHP callable for summary section. Optional.
	 *     @type array    $summary_args             Arguments passed to summary callback. Default [].
	 *     @type callable $footer_callback          PHP callable for custom footer. Default renders standard footer.
	 *     @type array    $footer_args              Arguments passed to footer callback. Default [].
	 *     @type string   $save_text                Save button text for create mode. Default 'Save'.
	 *     @type string   $update_text              Save button text for edit mode. Default 'Done'.
	 *     @type string   $saving_text              Save button text while saving. Default 'Saving...'.
	 *     @type string   $cancel_text              Cancel button text. Default 'Cancel'.
	 *     @type bool     $close_on_backdrop        Whether clicking backdrop closes drawer. Default true.
	 *     @type bool     $close_on_escape          Whether ESC key closes drawer. Default true.
	 *     @type bool     $dirty_check              Whether to guard against unsaved changes on close.
	 *                                              When true: backdrop click is disabled, Escape/X/Cancel
	 *                                              call confirmClose() instead of close(), and @input/@change
	 *                                              events on the content area call _markDirty(). Requires the
	 *                                              store to use affwp_drawer_dirty_tracking_alpine() methods.
	 *                                              Default false.
	 *     @type callable $header_callback          PHP callable for extra header content (after title/subtitle). Optional.
	 *     @type array    $header_args              Arguments passed to header callback. Default [].
	 *     @type callable $title_badge_callback     PHP callable rendered inline with the title (to the right of
	 *                                              the `<h2>`, before the pager/close-X column). Intended for a
	 *                                              status pill so state reads as "this record IS in X state",
	 *                                              not "this record has an X attribute." Receives `header_args`
	 *                                              as its single parameter. Ignored when editable_title is true.
	 *                                              Optional.
	 *     @type callable $header_actions_callback  PHP callable rendered in the header chrome, immediately left
	 *                                              of the close X. Use for drawer-scoped controls (e.g. Preview
	 *                                              toggle) that belong in the header rather than the body.
	 *                                              Receives `header_actions_args` as its single parameter. Optional.
	 *     @type array    $header_actions_args      Arguments passed to header_actions_callback. Default [].
	 *     @type bool     $editable_title           When true, the trait-rendered title becomes click-to-edit:
	 *                                              hovering reveals a pencil affordance, clicking swaps the
	 *                                              <h2> for an inline input. Requires the editable-title store
	 *                                              methods (see contract above). Default false.
	 * }
	 * @return string The rendered drawer HTML.
	 */
	public function render_drawer( $args = [] ) {
		$defaults = [
			'id'                      => '',
			'alpine_store'            => '',
			'width'                   => 'xl',
			'header_style'            => 'gradient',
			'content_callback'        => null,
			'content_args'            => [],
			'summary_callback'        => null,
			'summary_args'            => [],
			'footer_callback'         => null,
			'footer_args'             => [],
			'no_footer'               => false,
			'save_text'               => __( 'Save', 'affiliate-wp' ),
			'update_text'             => __( 'Done', 'affiliate-wp' ),
			'saving_text'             => __( 'Saving...', 'affiliate-wp' ),
			'cancel_text'             => __( 'Cancel', 'affiliate-wp' ),
			'loading_x_show'          => '',
			'loading_text'            => __( 'Loading...', 'affiliate-wp' ),
			'close_on_backdrop'       => true,
			'close_on_escape'         => true,
			'dirty_check'             => false,
			'header_callback'         => null,
			'header_args'             => [],
			'title_badge_callback'    => null,
			'header_actions_callback' => null,
			'header_actions_args'     => [],
			/*
			 * When true, the trait-rendered title becomes click-to-edit:
			 * hovering reveals a pencil affordance, clicking swaps the
			 * h2 for an inline input. The store must implement
			 * isEditingTitle, titleDraft, startEditTitle(), commitTitle(),
			 * and cancelTitle(). Disabled by default; drawers opt in only
			 * when their identity benefits from inline rename (kits, etc.).
			 */
			'editable_title'          => false,
		];

		$args = wp_parse_args( $args, $defaults );

		// Validate required fields.
		if ( empty( $args['id'] ) || empty( $args['alpine_store'] ) ) {
			return '';
		}

		$store      = esc_js( $args['alpine_store'] );
		$store_ref  = sprintf( '$store.%s', $store );
		$width      = isset( $this->drawer_widths[ $args['width'] ] ) ? $this->drawer_widths[ $args['width'] ] : $this->drawer_widths['xl'];
		$header     = isset( $this->drawer_header_styles[ $args['header_style'] ] ) ? $this->drawer_header_styles[ $args['header_style'] ] : $this->drawer_header_styles['gradient'];

		// When dirty_check is enabled, disable backdrop close — only intentional actions can close.
		$close_on_backdrop = $args['dirty_check'] ? false : $args['close_on_backdrop'];
		$close_on_escape   = $args['close_on_escape'];
		$dirty_check       = $args['dirty_check'];

		ob_start();
		?>

		<!--
			Drawer: <?php echo esc_attr( $args['id'] ); ?>

			x-data="{}" creates an empty Alpine component scope. Without it,
			Alpine skips all directives on descendants (x-show, x-cloak, @click)
			and the drawer never reacts to the store's isOpen state.
		-->
		<div id="<?php echo esc_attr( $args['id'] ); ?>" class="affwp-ui" x-data="{}">

			<!-- Backdrop -->
			<div x-cloak
				<?php /* Fade is a CSS transition driven by data-state, matching the panel slide. Alpine's x-transition snaps in Safari (display toggle + class swap across frames); a native opacity transition keyed off the attribute interpolates everywhere. pointer-events-none while closed lets page clicks pass through the still-mounted backdrop. */ ?>
				:data-state="<?php echo esc_attr( "{$store_ref}.isOpen ? 'open' : 'closed'" ); ?>"
				class="fixed inset-x-0 z-40 bg-slate-900/60 transition-opacity duration-300 ease-in-out opacity-0 pointer-events-none data-[state=open]:opacity-100 data-[state=open]:pointer-events-auto"
				style="top: var(--wp-admin--admin-bar--height, 32px); height: calc(100vh - var(--wp-admin--admin-bar--height, 32px));"
				<?php if ( $close_on_backdrop ) : ?>
					@click="<?php echo esc_attr( "{$store_ref}.close()" ); ?>"
				<?php endif; ?>>
			</div>

			<!-- Drawer Panel -->
			<?php
			/*
			 * Click-to-close pattern.
			 *
			 * Originally: panel had `@click.stop` to swallow descendant
			 * clicks, and the wrapper had `@click="close()"` to fire on
			 * backdrop clicks (anywhere outside the panel but inside the
			 * wrapper's padding zone).
			 *
			 * The .stop side-effect: it breaks native HTML Popover API
			 * light-dismiss for popovers rendered INSIDE the drawer
			 * (Switch payout method picker, for example). The browser
			 * needs the click event to bubble freely for its light-dismiss
			 * heuristics.
			 *
			 * New pattern: wrapper checks `target.closest('[data-affwp-drawer-panel]')`
			 * and only closes when the click was actually outside the
			 * panel. Panel no longer needs @click.stop, so events bubble
			 * normally and the Popover API works.
			 */
			$close_when_outside_panel = $close_on_backdrop
				? "if ( ! \$event.target.closest( '[data-affwp-drawer-panel]' ) ) {$store_ref}.close()"
				: '';
			?>
			<div x-cloak
				<?php /* Slide is a plain CSS transition driven by data-state, not Alpine's x-transition. Alpine toggles display + swaps classes across animation frames, which Safari intermittently fails to interpolate (the panel snaps open). A native transform transition keyed off the data-state attribute interpolates reliably in every browser. The panel stays mounted; :inert keeps it out of the tab order while closed so we don't need display:none (which would kill the leave animation). */ ?>
				:data-state="<?php echo esc_attr( "{$store_ref}.isOpen ? 'open' : 'closed'" ); ?>"
				:inert="<?php echo esc_attr( "! {$store_ref}.isOpen" ); ?>"
				<?php if ( $close_on_backdrop ) : ?>
					@click="<?php echo esc_attr( $close_when_outside_panel ); ?>"
				<?php endif; ?>
				<?php /* Pin the right edge to the visual viewport (100vw) instead of right:0. The scroll lock that opens the drawer removes the page scrollbar; on close it returns and would shove a right:0 panel ~15px left mid-slide. calc(100% - 100vw) cancels that scrollbar delta so the panel slides straight out. */ ?>
				class="fixed right-[calc(100%-100vw)] z-50 flex max-w-full pl-10 sm:pl-16 transition-transform duration-300 ease-in-out transform-[translateX(100%)] data-[state=open]:transform-[translateX(0)]"
				<?php if ( $close_on_escape ) : ?>
					<?php if ( $dirty_check ) : ?>
						@keydown.escape.window="<?php echo esc_attr( "if ({$store_ref}.isOpen) { {$store_ref}.confirmClose(); }" ); ?>"
					<?php else : ?>
						@keydown.escape.window="<?php echo esc_attr( "if ({$store_ref}.isOpen) { {$store_ref}.close(); }" ); ?>"
					<?php endif; ?>
				<?php endif; ?>
				style="top: var(--wp-admin--admin-bar--height, 32px); height: calc(100vh - var(--wp-admin--admin-bar--height, 32px));">
				<div class="h-full w-screen pl-10 sm:pl-16 transition-[max-width] duration-300 ease-in-out"
					:class="<?php echo esc_attr( "{$store_ref}.drawerWidth || '{$width}'" ); ?>">
					<div
						x-trap.inert.noscroll.noautofocus="<?php echo esc_attr( "{$store_ref}.isOpen" ); ?>"
						class="relative flex h-full flex-col overflow-hidden bg-white shadow-2xl"
						role="dialog"
						aria-modal="true"
						aria-labelledby="<?php echo esc_attr( 'affwp-drawer-title-' . $args['id'] ); ?>"
						data-affwp-drawer-panel
					>

						<?php if ( ! empty( $args['loading_x_show'] ) ) : ?>
						<!-- Full-drawer loading overlay -->
						<div x-show="<?php echo esc_attr( $args['loading_x_show'] ); ?>" x-cloak
							class="absolute inset-0 z-10 flex flex-col items-center justify-center bg-white">
							<svg class="animate-spin h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24">
								<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
								<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
							</svg>
							<p class="text-sm text-gray-500 mt-3 m-0"><?php echo esc_html( $args['loading_text'] ); ?></p>
						</div>
						<?php endif; ?>

						<!-- Header -->
						<div class="<?php echo esc_attr( $header['bg'] ); ?> px-6 py-4">
							<div class="flex items-start justify-between">
								<div class="min-w-0 flex-1">
									<?php if ( $args['editable_title'] ) : ?>
										<!--
											Click-to-edit title. Display mode: h2 + ghost
											pencil that fades in on group-hover for
											discoverability. Edit mode: input pre-filled
											with current title; Enter saves, Escape cancels,
											blur saves. The store owns the mode flag and
											the draft buffer.
										-->
										<div class="group flex items-center gap-2 -ml-1.5">
											<h2
												x-show="!<?php echo esc_attr( "{$store_ref}.isEditingTitle" ); ?>"
												x-on:click="<?php echo esc_attr( "{$store_ref}.startEditTitle()" ); ?>"
												class="text-lg font-semibold <?php echo esc_attr( $header['title_color'] ); ?> cursor-text px-1.5 py-0.5 -my-0.5 rounded hover:bg-gray-100 transition truncate"
												x-text="<?php echo esc_attr( "{$store_ref}.title" ); ?>"
												title="<?php esc_attr_e( 'Click to rename', 'affiliate-wp' ); ?>"
											></h2>
											<svg
												x-show="!<?php echo esc_attr( "{$store_ref}.isEditingTitle" ); ?>"
												x-on:click="<?php echo esc_attr( "{$store_ref}.startEditTitle()" ); ?>"
												class="w-3.5 h-3.5 text-gray-400 opacity-0 group-hover:opacity-100 cursor-pointer transition shrink-0"
												viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"
												aria-hidden="true"
											>
												<path d="M11 2 14 5l-9 9H2v-3l9-9zM10 3l3 3"/>
											</svg>
											<input
												x-show="<?php echo esc_attr( "{$store_ref}.isEditingTitle" ); ?>"
												x-cloak
												x-model="<?php echo esc_attr( "{$store_ref}.titleDraft" ); ?>"
												x-on:keydown.enter.prevent="<?php echo esc_attr( "{$store_ref}.commitTitle()" ); ?>"
												x-on:keydown.escape.prevent="<?php echo esc_attr( "{$store_ref}.cancelTitle()" ); ?>"
												x-on:blur="<?php echo esc_attr( "{$store_ref}.commitTitle()" ); ?>"
												x-effect="<?php echo esc_attr( "if ({$store_ref}.isEditingTitle) \$nextTick(() => \$el.select())" ); ?>"
												type="text"
												maxlength="200"
												class="text-lg font-semibold w-full px-1.5 py-0.5 -my-0.5 rounded border border-affwp-brand-500 ring-2 ring-affwp-brand-500/20 focus:outline-none bg-white text-gray-900"
											/>
										</div>
									<?php else : ?>
										<!--
											Title row is a flex container so the optional
											title_badge slot can sit inline with the <h2>
											instead of below it. Matches GitHub / Linear /
											Stripe detail-page pattern: state is adjacent
											to the identifier, not stacked underneath.
										-->
										<div class="flex items-center gap-2">
											<h2 id="<?php echo esc_attr( 'affwp-drawer-title-' . $args['id'] ); ?>"
												class="text-lg font-semibold <?php echo esc_attr( $header['title_color'] ); ?> m-0"
												x-text="<?php echo esc_attr( "{$store_ref}.title" ); ?>">
											</h2>
											<?php
											if ( is_callable( $args['title_badge_callback'] ) ) {
												call_user_func( $args['title_badge_callback'], $args['header_args'] );
											}
											?>
										</div>
									<?php endif; ?>
									<p class="mt-1 text-sm <?php echo esc_attr( $header['subtitle_color'] ); ?>"
										x-text="<?php echo esc_attr( "{$store_ref}.subtitle" ); ?>"
										x-show="<?php echo esc_attr( "{$store_ref}.subtitle" ); ?>">
									</p>
									<?php
									if ( is_callable( $args['header_callback'] ) ) {
										call_user_func( $args['header_callback'], $args['header_args'] );
									}
									?>
								</div>
								<div class="flex items-center gap-2 shrink-0">
									<?php
									/*
									 * Optional header action slot, rendered immediately
									 * left of the close X. For drawer-scoped controls
									 * (Preview toggle, etc.) that belong in the chrome
									 * rather than the body.
									 */
									if ( is_callable( $args['header_actions_callback'] ) ) {
										call_user_func( $args['header_actions_callback'], $args['header_actions_args'] );
									}
									?>
									<?php
									/*
									 * Drawer-shell close X. Hidden when the consuming
									 * store sets hideHeaderClose: true. Used by drawers
									 * that take over their own body for a focused
									 * sub-task (template picker, preview maximize),
									 * where a drawer-level "close everything" affordance
									 * competes with the sub-task's own back/cancel
									 * button. The store opting in is the cleanest
									 * decoupling: this trait stays unaware of any
									 * specific drawer's state.
									 *
									 * The 'x-button' source argument lets store close()
									 * implementations differentiate the X click from
									 * Esc keypress. Drawers that cascade through
									 * sub-states on Esc (template picker, preview
									 * maximize) should treat the X as a request to
									 * close the entire drawer in one click; sub-state
									 * cascading is a keyboard-only convention. Stores
									 * whose close() doesn't accept arguments simply
									 * ignore the extra positional arg, so this is
									 * backward-compatible.
									 */
									?>
									<button type="button"
										x-show="<?php echo esc_attr( "!{$store_ref}.hideHeaderClose" ); ?>"
										@click="<?php echo esc_attr( $dirty_check ? "{$store_ref}.confirmClose('x-button')" : "{$store_ref}.close('x-button')" ); ?>"
										class="rounded-md <?php echo esc_attr( "{$header['close_bg']} {$header['close_color']}" ); ?> p-1.5 cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 <?php echo esc_attr( $header['close_ring'] ); ?>">
										<span class="sr-only"><?php esc_html_e( 'Close drawer', 'affiliate-wp' ); ?></span>
										<?php \AffiliateWP\Utils\Icons::render( 'x-mark', '', [ 'width' => '20', 'height' => '20' ] ); ?>
									</button>
								</div>
							</div>
						</div>

						<!-- General Error Banner (field-level errors are shown inline) -->
						<div x-show="<?php echo esc_attr( "{$store_ref}.errors?.general" ); ?>"
							x-collapse
							class="border-l-4 border-red-400 bg-red-50 px-4 py-3">
							<div class="flex items-center gap-2.5">
								<div class="flex-shrink-0">
									<?php \AffiliateWP\Utils\Icons::render( 'circle-warning', '', [ 'class' => 'h-5 w-5 text-red-400' ] ); ?>
								</div>
								<p class="text-sm font-medium text-red-800 m-0"
									x-text="<?php echo esc_attr( "{$store_ref}.errors.general" ); ?>"></p>
							</div>
						</div>

						<!-- Scrollable Content Area -->
						<div class="flex-1 overflow-y-auto"
							<?php if ( $dirty_check ) : ?>
								@input="<?php echo esc_attr( "{$store_ref}._markDirty()" ); ?>"
								@change="<?php echo esc_attr( "{$store_ref}._markDirty()" ); ?>"
							<?php endif; ?>>
							<?php
							/*
							 * Drawer body padding/spacing. Drawers that take over
							 * their body for a focused mode (creative drawer's
							 * preview maximize, template picker) can drop padding
							 * so takeover content can extend edge-to-edge. Nullish
							 * coalescing honors empty string as "no classes," while
							 * undefined falls back to the default.
							 */
							?>
							<div :class="<?php echo esc_attr( "{$store_ref}.contentClass ?? 'px-6 py-6 space-y-6'" ); ?>"
								id="<?php echo esc_attr( $args['id'] . '-content' ); ?>">
								<?php
								if ( is_callable( $args['content_callback'] ) ) {
									call_user_func( $args['content_callback'], $args['content_args'] );
								}
								?>
							</div>
						</div>

						<?php if ( is_callable( $args['summary_callback'] ) ) : ?>
							<!-- Summary Section -->
							<?php call_user_func( $args['summary_callback'], $args['summary_args'] ); ?>
						<?php endif; ?>

						<?php if ( ! empty( $args['no_footer'] ) ) : ?>
							<!--
								Footer suppressed entirely (no_footer=true).
								Body scroll region extends to the bottom of
								the drawer panel. Used when every actionable
								affordance the drawer offers lives inside its
								body cards (e.g. payout drawer's Next Steps +
								Switch payout method), making a footer Close
								button redundant — Escape, the X in the header,
								and outside-click already close the drawer.
							-->
						<?php elseif ( is_callable( $args['footer_callback'] ) ) : ?>
							<!-- Custom Footer -->
							<div class="flex-shrink-0 border-t border-gray-200 px-6 py-4"
								<?php if ( ! empty( $args['footer_x_show'] ) ) : ?>
									x-show="<?php echo esc_attr( $args['footer_x_show'] ); ?>"
								<?php endif; ?>
							>
								<?php call_user_func( $args['footer_callback'], $args['footer_args'] ); ?>
							</div>
						<?php else : ?>
							<!-- Default Footer -->
							<?php echo $this->build_drawer_footer( $args, $store_ref ); ?>
						<?php endif; ?>

					</div>
				</div>
			</div>

		</div>
		<?php

		return ob_get_clean();
	}

	/**
	 * Build the default drawer footer.
	 *
	 * Renders a standard footer with Cancel and Save/Done buttons,
	 * with loading state support.
	 *
	 * @since 2.33.0
	 *
	 * @param array  $args      Drawer arguments.
	 * @param string $store_ref Alpine store reference string.
	 * @return string Footer HTML.
	 */
	protected function build_drawer_footer( $args, $store_ref ) {
		ob_start();
		?>
		<div class="flex-shrink-0 border-t border-gray-200 px-6 py-4">
			<div class="flex flex-row-reverse items-center justify-start gap-3">
				<!-- Save Button (first in DOM = first tab stop) -->
				<button type="button"
					@click="<?php echo esc_attr( "{$store_ref}.save()" ); ?>"
					:disabled="<?php echo esc_attr( "{$store_ref}.isLoading" ); ?>"
					class="cursor-pointer inline-flex items-center rounded-md border border-transparent bg-affwp-brand-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-affwp-brand-600 focus:outline-none focus:ring-2 focus:ring-affwp-brand-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
					<!-- Loading Spinner -->
					<svg x-show="<?php echo esc_attr( "{$store_ref}.isLoading" ); ?>"
						x-cloak
						class="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
						xmlns="http://www.w3.org/2000/svg"
						fill="none"
						viewBox="0 0 24 24">
						<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
						<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
					</svg>
					<span x-text="<?php echo esc_attr( sprintf(
						"%s.isLoading ? '%s' : (%s.isNew ? '%s' : '%s')",
						$store_ref,
						esc_js( $args['saving_text'] ),
						$store_ref,
						esc_js( $args['save_text'] ),
						esc_js( $args['update_text'] )
					) ); ?>">
						<?php echo esc_html( $args['save_text'] ); ?>
					</span>
				</button>

				<!-- Cancel Button -->
				<button type="button"
					@click="<?php echo esc_attr( $args['dirty_check'] ? "{$store_ref}.confirmClose()" : "{$store_ref}.close()" ); ?>"
					class="cursor-pointer rounded-md bg-transparent border-0 px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-affwp-brand-500 focus:ring-offset-2 transition-colors">
					<?php echo esc_html( $args['cancel_text'] ); ?>
				</button>
			</div>
		</div>
		<?php
		return ob_get_clean();
	}
}
