{"version":3,"sources":["jsdelivr-header.js","/npm/@frybynite/image-cloud@0.10.1/src/config/defaults.ts","/npm/@frybynite/image-cloud@0.10.1/src/engines/AnimationEngine.ts","/npm/@frybynite/image-cloud@0.10.1/src/engines/PathAnimator.ts","/npm/@frybynite/image-cloud@0.10.1/src/engines/EntryAnimationEngine.ts","/npm/@frybynite/image-cloud@0.10.1/src/engines/IdleAnimationEngine.ts","/npm/@frybynite/image-cloud@0.10.1/src/layouts/RandomPlacementLayout.ts","/npm/@frybynite/image-cloud@0.10.1/src/layouts/RadialPlacementLayout.ts","/npm/@frybynite/image-cloud@0.10.1/src/layouts/GridPlacementLayout.ts","/npm/@frybynite/image-cloud@0.10.1/src/layouts/SpiralPlacementLayout.ts","/npm/@frybynite/image-cloud@0.10.1/src/layouts/ClusterPlacementLayout.ts","/npm/@frybynite/image-cloud@0.10.1/src/layouts/WavePlacementLayout.ts","/npm/@frybynite/image-cloud@0.10.1/src/utils/hexagonGeometry.ts","/npm/@frybynite/image-cloud@0.10.1/src/layouts/HoneycombPlacementLayout.ts","/npm/@frybynite/image-cloud@0.10.1/src/engines/LayoutEngine.ts","/npm/@frybynite/image-cloud@0.10.1/src/config/types.ts","/npm/@frybynite/image-cloud@0.10.1/src/utils/clipPathGenerator.ts","/npm/@frybynite/image-cloud@0.10.1/src/utils/styleUtils.ts","/npm/@frybynite/image-cloud@0.10.1/src/engines/ZoomEngine.ts","/npm/@frybynite/image-cloud@0.10.1/src/engines/SwipeEngine.ts","/npm/@frybynite/image-cloud@0.10.1/src/loaders/GoogleDriveLoader.ts","/npm/@frybynite/image-cloud@0.10.1/src/loaders/StaticImageLoader.ts","/npm/@frybynite/image-cloud@0.10.1/src/loaders/CompositeLoader.ts","/npm/@frybynite/image-cloud@0.10.1/src/loaders/ImageFilter.ts","/npm/@frybynite/image-cloud@0.10.1/src/styles/functionalStyles.ts","/npm/@frybynite/image-cloud@0.10.1/src/ImageCloud.ts","/npm/@frybynite/image-cloud@0.10.1/src/index.ts"],"names":[],"mappings":"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","file":"/npm/@frybynite/image-cloud@0.10.1/dist/image-cloud.umd.js","sourceRoot":"","sourcesContent":["/**\n * Minified by jsDelivr using Terser v5.39.0.\n * Original file: /npm/@frybynite/image-cloud@0.10.1/dist/image-cloud.umd.js\n *\n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n","/**\n * Default configuration for Image Gallery\n * Centralized settings for animation, layout, and API configuration\n */\n\nimport type { ImageCloudConfig, ImageCloudOptions, DeepPartial, ImageStylingConfig, ImageStyleState, ShadowPreset, WaveAlgorithmConfig, HoneycombAlgorithmConfig, RadialAlgorithmConfig, BouncePathConfig, ElasticPathConfig, WavePathConfig, BouncePreset, ElasticPreset, WavePathPreset, EntryPathConfig, EntryRotationConfig, EntryScaleConfig, ImageConfig, ImageSizingConfig, ImageRotationConfig, ImageVarianceConfig, ResponsiveBreakpoints, SharedLoaderConfig, ConfigSection, LoaderEntry, DebugConfig, IdleWiggleConfig, IdlePulseConfig, IdleBlinkConfig, IdleSpinConfig, IdleAnimationConfig } from './types';\n\n/**\n * Shadow presets for image styling\n */\nexport const SHADOW_PRESETS: Record<ShadowPreset, string> = Object.freeze({\n  'none': 'none',\n  'sm': '0 2px 4px rgba(0,0,0,0.1)',\n  'md': '0 4px 16px rgba(0,0,0,0.4)',\n  'lg': '0 8px 32px rgba(0,0,0,0.5)',\n  'glow': '0 0 30px rgba(255,255,255,0.6)'\n});\n\n/**\n * Bounce path presets - overshoot and settle animations\n */\nexport const BOUNCE_PRESETS: Record<BouncePreset, BouncePathConfig> = Object.freeze({\n  energetic: Object.freeze({ overshoot: 0.25, bounces: 2, decayRatio: 0.5 }),\n  playful: Object.freeze({ overshoot: 0.15, bounces: 1, decayRatio: 0.5 }),\n  subtle: Object.freeze({ overshoot: 0.08, bounces: 1, decayRatio: 0.5 })\n});\n\n/**\n * Elastic path presets - spring-like oscillation animations\n */\nexport const ELASTIC_PRESETS: Record<ElasticPreset, ElasticPathConfig> = Object.freeze({\n  gentle: Object.freeze({ stiffness: 150, damping: 30, mass: 1, oscillations: 2 }),\n  bouncy: Object.freeze({ stiffness: 300, damping: 15, mass: 1, oscillations: 4 }),\n  wobbly: Object.freeze({ stiffness: 180, damping: 12, mass: 1.5, oscillations: 5 }),\n  snappy: Object.freeze({ stiffness: 400, damping: 25, mass: 0.8, oscillations: 2 })\n});\n\n/**\n * Wave path presets - sinusoidal path animations\n */\nexport const WAVE_PATH_PRESETS: Record<WavePathPreset, WavePathConfig> = Object.freeze({\n  gentle: Object.freeze({ amplitude: 30, frequency: 1.5, decay: true, decayRate: 0.9, phase: 0 }),\n  playful: Object.freeze({ amplitude: 50, frequency: 2.5, decay: true, decayRate: 0.7, phase: 0 }),\n  serpentine: Object.freeze({ amplitude: 60, frequency: 3, decay: false, decayRate: 1, phase: 0 }),\n  flutter: Object.freeze({ amplitude: 20, frequency: 4, decay: true, decayRate: 0.5, phase: 0 })\n});\n\n/**\n * Default path configuration (linear - no special path effects)\n */\nexport const DEFAULT_PATH_CONFIG: EntryPathConfig = Object.freeze({\n  type: 'linear' as const\n});\n\n/**\n * Default entry rotation configuration (no rotation animation)\n */\nexport const DEFAULT_ENTRY_ROTATION: EntryRotationConfig = Object.freeze({\n  mode: 'none' as const\n});\n\n/**\n * Default entry scale configuration (no scale animation)\n */\nexport const DEFAULT_ENTRY_SCALE: EntryScaleConfig = Object.freeze({\n  mode: 'none' as const\n});\n\n\n/**\n * Default image styling configuration\n */\nexport const DEFAULT_STYLING: ImageStylingConfig = Object.freeze({\n  default: Object.freeze({\n    border: Object.freeze({\n      width: 0,\n      color: '#000000',\n      radius: 0,\n      style: 'solid' as const\n    }),\n    shadow: 'none' as ShadowPreset,\n    clipPath: undefined,\n    filter: Object.freeze({}),\n    opacity: 1,\n    cursor: 'pointer',\n    outline: Object.freeze({\n      width: 0,\n      color: '#000000',\n      style: 'solid' as const,\n      offset: 0\n    })\n  }),\n  hover: Object.freeze({\n    shadow: 'none' as ShadowPreset\n  }),\n  focused: Object.freeze({\n    shadow: 'none' as ShadowPreset\n  })\n});\n\n/**\n * Default radial layout configuration\n */\nexport const DEFAULT_RADIAL_CONFIG: RadialAlgorithmConfig = Object.freeze({\n  tightness: 1.0,\n});\n\n/**\n * Default wave layout configuration\n */\nexport const DEFAULT_WAVE_CONFIG: WaveAlgorithmConfig = Object.freeze({\n  rows: 1,\n  amplitude: 100,\n  frequency: 2,\n  phaseShift: 0,\n  synchronization: 'offset' as const\n  // Note: Image rotation along wave is now controlled via image.rotation.mode = 'tangent'\n});\n\nexport const DEFAULT_HONEYCOMB_CONFIG: HoneycombAlgorithmConfig = Object.freeze({\n  spacing: 0\n});\n\n/**\n * Default responsive breakpoints for layout\n */\nexport const DEFAULT_RESPONSIVE_BREAKPOINTS: ResponsiveBreakpoints = Object.freeze({\n  mobile: Object.freeze({ maxWidth: 767 }),\n  tablet: Object.freeze({ maxWidth: 1199 })\n});\n\n/**\n * Default image sizing configuration\n */\nexport const DEFAULT_IMAGE_SIZING: ImageSizingConfig = Object.freeze({\n  mode: 'adaptive' as const,  // Default to adaptive sizing\n  minSize: 50,                // Adaptive mode minimum\n  maxSize: 400,               // Adaptive mode maximum\n  variance: Object.freeze({\n    min: 1.0,  // No variance by default\n    max: 1.0\n  })\n});\n\n/**\n * Default image rotation configuration\n */\nexport const DEFAULT_IMAGE_ROTATION: ImageRotationConfig = Object.freeze({\n  mode: 'none' as const,\n  range: Object.freeze({\n    min: -15,\n    max: 15\n  })\n});\n\n/**\n * Default image configuration\n */\nexport const DEFAULT_IMAGE_CONFIG: ImageConfig = Object.freeze({\n  sizing: DEFAULT_IMAGE_SIZING,\n  rotation: DEFAULT_IMAGE_ROTATION\n});\n\n/**\n * Default configuration object\n * Frozen to prevent accidental modifications\n */\n/**\n * Default shared loader configuration\n */\nexport const DEFAULT_SHARED_LOADER_CONFIG: SharedLoaderConfig = Object.freeze({\n  validateUrls: true,\n  validationTimeout: 5000,\n  validationMethod: 'head' as const,\n  allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']\n});\n\n/**\n * Default debug configuration\n */\nexport const DEFAULT_DEBUG_CONFIG: DebugConfig = Object.freeze({\n  enabled: false,\n  centers: false,\n  loaders: false\n});\n\nexport const DEFAULT_IDLE_WIGGLE: IdleWiggleConfig = Object.freeze({ maxAngle: 5, speed: 2000, sync: 'random' as const });\nexport const DEFAULT_IDLE_PULSE: IdlePulseConfig = Object.freeze({ minScale: 0.95, maxScale: 1.05, speed: 2400, sync: 'random' as const });\nexport const DEFAULT_IDLE_BLINK: IdleBlinkConfig = Object.freeze({ onRatio: 0.7, speed: 3000, style: 'snap' as const });\nexport const DEFAULT_IDLE_SPIN: IdleSpinConfig = Object.freeze({ speed: 4000, direction: 'clockwise' as const });\nexport const DEFAULT_IDLE_CONFIG: IdleAnimationConfig = Object.freeze({ type: 'none' as const });\n\nexport const DEFAULT_CONFIG: ImageCloudConfig = Object.freeze({\n  // Loader configuration (always an array, composite behavior is implicit)\n  loaders: [] as LoaderEntry[],\n\n  // Shared loader settings and debug config\n  config: Object.freeze({\n    loaders: DEFAULT_SHARED_LOADER_CONFIG,\n    debug: DEFAULT_DEBUG_CONFIG\n  }),\n\n  // Image sizing and rotation configuration\n  image: DEFAULT_IMAGE_CONFIG,\n\n  // Pattern-based layout configuration\n  layout: Object.freeze({\n    algorithm: 'radial' as const,\n    scaleDecay: 0,                 // No decay by default (0-1 for radial/spiral)\n    responsive: DEFAULT_RESPONSIVE_BREAKPOINTS,\n    targetCoverage: 0.6,           // Target 60% of container area\n    densityFactor: 1.0,            // Default density\n    spacing: Object.freeze({\n      padding: 50   // padding from viewport edges\n    })\n  }),\n\n  // Pattern-based animation configuration\n  animation: Object.freeze({\n    duration: 600,  // milliseconds\n    easing: Object.freeze({\n      default: 'cubic-bezier(0.4, 0.0, 0.2, 1)',  // smooth easing\n      bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',  // bounce effect\n      focus: 'cubic-bezier(0.4, 0.0, 0.2, 1)'  // focus/zoom easing\n    }),\n    queue: Object.freeze({\n      enabled: true,  // When false, all images display simultaneously\n      interval: 150,  // ms between processing queue items (when enabled)\n    }),\n    entry: Object.freeze({\n      start: Object.freeze({\n        position: 'nearest-edge' as const,  // Default to nearest edge (current behavior)\n        offset: 100,  // pixels beyond edge\n        circular: Object.freeze({\n          radius: '120%',  // 120% of container diagonal\n          distribution: 'even' as const\n        })\n      }),\n      timing: Object.freeze({\n        duration: 600  // ms\n      }),\n      easing: 'cubic-bezier(0.25, 1, 0.5, 1)',  // smooth deceleration\n      path: DEFAULT_PATH_CONFIG,\n      rotation: DEFAULT_ENTRY_ROTATION,\n      scale: DEFAULT_ENTRY_SCALE\n    }),\n    idle: DEFAULT_IDLE_CONFIG\n  }),\n\n  // Pattern-based interaction configuration\n  interaction: Object.freeze({\n    focus: Object.freeze({\n      scalePercent: 0.8,  // 80% of container height\n      zIndex: 1000,\n      animationDuration: undefined  // Use default animation duration\n    }),\n    navigation: Object.freeze({\n      keyboard: true,\n      swipe: true,\n      mouseWheel: undefined  // STUB: Not implemented yet\n    }),\n    dragging: true\n  }),\n\n  // UI configuration\n  ui: Object.freeze({\n    showLoadingSpinner: false,\n    showImageCounter: false,\n    showNavButtons: false,\n    showFocusOutline: false\n  }),\n\n  // Image styling\n  styling: DEFAULT_STYLING\n});\n\n/**\n * Deep merge a single style state (border, filter, outline, etc.)\n */\nfunction deepMergeStyleState(\n  base: ImageStyleState | undefined,\n  override: Partial<ImageStyleState> | undefined\n): ImageStyleState {\n  if (!base) return override as ImageStyleState || {};\n  if (!override) return { ...base };\n\n  const merged: ImageStyleState = { ...base };\n\n  // Merge border\n  if (override.border !== undefined) {\n    merged.border = { ...base.border, ...override.border };\n  }\n\n  // Merge per-side borders\n  if (override.borderTop !== undefined) {\n    merged.borderTop = { ...base.borderTop, ...override.borderTop };\n  }\n  if (override.borderRight !== undefined) {\n    merged.borderRight = { ...base.borderRight, ...override.borderRight };\n  }\n  if (override.borderBottom !== undefined) {\n    merged.borderBottom = { ...base.borderBottom, ...override.borderBottom };\n  }\n  if (override.borderLeft !== undefined) {\n    merged.borderLeft = { ...base.borderLeft, ...override.borderLeft };\n  }\n\n  // Merge filter\n  if (override.filter !== undefined) {\n    merged.filter = { ...base.filter, ...override.filter };\n  }\n\n  // Merge outline\n  if (override.outline !== undefined) {\n    merged.outline = { ...base.outline, ...override.outline };\n  }\n\n  // Override simple properties\n  if (override.shadow !== undefined) merged.shadow = override.shadow;\n  if (override.clipPath !== undefined) merged.clipPath = override.clipPath;\n  if (override.opacity !== undefined) merged.opacity = override.opacity;\n  if (override.cursor !== undefined) merged.cursor = override.cursor;\n  if (override.className !== undefined) merged.className = override.className;\n  if (override.objectFit !== undefined) merged.objectFit = override.objectFit;\n  if (override.aspectRatio !== undefined) merged.aspectRatio = override.aspectRatio;\n\n  // Override per-corner border radius\n  if (override.borderRadiusTopLeft !== undefined) merged.borderRadiusTopLeft = override.borderRadiusTopLeft;\n  if (override.borderRadiusTopRight !== undefined) merged.borderRadiusTopRight = override.borderRadiusTopRight;\n  if (override.borderRadiusBottomRight !== undefined) merged.borderRadiusBottomRight = override.borderRadiusBottomRight;\n  if (override.borderRadiusBottomLeft !== undefined) merged.borderRadiusBottomLeft = override.borderRadiusBottomLeft;\n\n  return merged;\n}\n\n/**\n * Deep merge styling config with proper state inheritance\n * - hover inherits from default, then applies overrides\n * - focused inherits from default, then applies overrides\n */\nfunction deepMergeStyling(\n  defaults: ImageStylingConfig,\n  userStyling: Partial<ImageStylingConfig> | undefined\n): ImageStylingConfig {\n  if (!userStyling) return { ...defaults };\n\n  // First, merge the default state\n  const mergedDefault = deepMergeStyleState(defaults.default, userStyling.default);\n\n  // Hover inherits from merged default, then user hover overrides\n  const mergedHover = deepMergeStyleState(\n    deepMergeStyleState(mergedDefault, defaults.hover),\n    userStyling.hover\n  );\n\n  // Focused inherits from merged default, then user focused overrides\n  const mergedFocused = deepMergeStyleState(\n    deepMergeStyleState(mergedDefault, defaults.focused),\n    userStyling.focused\n  );\n\n  return {\n    default: mergedDefault,\n    hover: mergedHover,\n    focused: mergedFocused\n  };\n}\n\n/**\n * Deep merge utility for config objects\n * Merges user config with default config\n */\n/**\n * Deep merge image config with validation\n */\nfunction deepMergeImageConfig(\n  defaults: ImageConfig,\n  userImage: Partial<ImageConfig> | undefined\n): ImageConfig {\n  if (!userImage) return { ...defaults };\n\n  const merged: ImageConfig = { ...defaults };\n\n  // Deep merge sizing config\n  if (userImage.sizing !== undefined) {\n    merged.sizing = {\n      ...defaults.sizing,\n      ...userImage.sizing\n    };\n\n    // Deep merge variance with validation (min: 0.25-1, max: 1-1.75)\n    if (userImage.sizing.variance) {\n      const userVariance = userImage.sizing.variance as ImageVarianceConfig;\n      const validMin = userVariance.min !== undefined && userVariance.min >= 0.25 && userVariance.min <= 1\n        ? userVariance.min\n        : defaults.sizing?.variance?.min ?? 1.0;\n      const validMax = userVariance.max !== undefined && userVariance.max >= 1 && userVariance.max <= 1.75\n        ? userVariance.max\n        : defaults.sizing?.variance?.max ?? 1.0;\n      merged.sizing!.variance = { min: validMin, max: validMax };\n    }\n  }\n\n  // Deep merge rotation config\n  if (userImage.rotation !== undefined) {\n    merged.rotation = {\n      ...defaults.rotation,\n      ...userImage.rotation\n    };\n\n    // Deep merge rotation range with validation\n    if (userImage.rotation.range) {\n      const userRange = userImage.rotation.range;\n      const validMin = userRange.min !== undefined && userRange.min >= -180 && userRange.min <= 0\n        ? userRange.min\n        : defaults.rotation?.range?.min ?? -15;\n      const validMax = userRange.max !== undefined && userRange.max >= 0 && userRange.max <= 180\n        ? userRange.max\n        : defaults.rotation?.range?.max ?? 15;\n      merged.rotation!.range = { min: validMin, max: validMax };\n    }\n  }\n\n  return merged;\n}\n\n/**\n * Convert legacy layout.rotation config to new image.rotation format\n * This provides backward compatibility with the old API\n */\nfunction convertLegacyRotationConfig(userConfig: DeepPartial<ImageCloudConfig>): Partial<ImageConfig> | undefined {\n  const legacyRotation = (userConfig.layout as any)?.rotation;\n  if (!legacyRotation) return undefined;\n\n  // Legacy format: { enabled: boolean, range: { min, max } }\n  if ('enabled' in legacyRotation) {\n    return {\n      rotation: {\n        mode: legacyRotation.enabled ? 'random' : 'none',\n        range: legacyRotation.range\n      }\n    };\n  }\n\n  return undefined;\n}\n\n/**\n * Convert legacy layout.sizing.variance config to new image.sizing.variance format\n */\nfunction convertLegacyVarianceConfig(userConfig: DeepPartial<ImageCloudConfig>): Partial<ImageConfig> | undefined {\n  const legacyVariance = (userConfig.layout as any)?.sizing?.variance;\n  if (!legacyVariance) return undefined;\n\n  return {\n    sizing: {\n      mode: 'adaptive',  // Legacy variance config implies adaptive mode\n      variance: legacyVariance\n    }\n  };\n}\n\nexport function mergeConfig(\n  userConfig: ImageCloudOptions = {}\n): ImageCloudConfig {\n  // Convert legacy configs to new format\n  const legacyRotation = convertLegacyRotationConfig(userConfig as any);\n  const legacyVariance = convertLegacyVarianceConfig(userConfig as any);\n\n  // Combine user image config with converted legacy configs\n  // User's explicit image config takes precedence over legacy conversions\n  let combinedImageConfig: Partial<ImageConfig> | undefined = userConfig.image as Partial<ImageConfig> | undefined;\n  if (legacyRotation || legacyVariance) {\n    combinedImageConfig = {\n      ...(legacyVariance || {}),\n      ...(legacyRotation || {}),\n      ...combinedImageConfig\n    };\n    // Deep merge the rotation config if both exist\n    if (combinedImageConfig.rotation && legacyRotation?.rotation && userConfig.image?.rotation) {\n      combinedImageConfig.rotation = {\n        ...legacyRotation.rotation,\n        ...(userConfig.image as any).rotation\n      };\n    }\n  }\n\n  // Build loaders array: images shorthand prepended, then explicit loaders\n  const loaders = [...(userConfig.loaders ?? [])];\n  if (userConfig.images && userConfig.images.length > 0) {\n    loaders.unshift({\n      static: {\n        sources: [{ urls: userConfig.images }]\n      }\n    });\n  }\n\n  // Merge shared loader config\n  const sharedLoaderConfig: SharedLoaderConfig = {\n    ...DEFAULT_SHARED_LOADER_CONFIG,\n    ...(userConfig.config?.loaders ?? {})\n  };\n\n  const mergedConfig: ConfigSection = {\n    loaders: sharedLoaderConfig\n  };\n\n  const merged: ImageCloudConfig = {\n    loaders,\n    config: mergedConfig,\n    image: deepMergeImageConfig(DEFAULT_IMAGE_CONFIG, combinedImageConfig),\n    layout: { ...DEFAULT_CONFIG.layout },\n    animation: { ...DEFAULT_CONFIG.animation },\n    interaction: { ...DEFAULT_CONFIG.interaction },\n    ui: { ...DEFAULT_CONFIG.ui },\n    styling: deepMergeStyling(DEFAULT_STYLING, userConfig.styling as Partial<ImageStylingConfig> | undefined)\n  };\n\n  // Deep merge layout config\n  if (userConfig.layout) {\n    merged.layout = {\n      ...DEFAULT_CONFIG.layout,\n      ...userConfig.layout\n    } as any;\n\n    // Deep merge responsive breakpoints config\n    if (userConfig.layout.responsive) {\n      merged.layout.responsive = {\n        ...DEFAULT_CONFIG.layout.responsive!,\n        mobile: userConfig.layout.responsive.mobile\n          ? { ...DEFAULT_CONFIG.layout.responsive!.mobile, ...userConfig.layout.responsive.mobile }\n          : DEFAULT_CONFIG.layout.responsive!.mobile,\n        tablet: userConfig.layout.responsive.tablet\n          ? { ...DEFAULT_CONFIG.layout.responsive!.tablet, ...userConfig.layout.responsive.tablet }\n          : DEFAULT_CONFIG.layout.responsive!.tablet\n      };\n    }\n\n    // Deep merge spacing config\n    if (userConfig.layout.spacing) {\n      merged.layout.spacing = {\n        ...DEFAULT_CONFIG.layout.spacing,\n        ...userConfig.layout.spacing\n      };\n    }\n  }\n\n  // Deep merge animation config\n  if (userConfig.animation) {\n    merged.animation = {\n      ...DEFAULT_CONFIG.animation,\n      ...userConfig.animation\n    } as any;\n\n    // Deep merge easing config\n    if (userConfig.animation.easing) {\n      merged.animation.easing = {\n        ...DEFAULT_CONFIG.animation.easing,\n        ...userConfig.animation.easing\n      };\n    }\n\n    // Deep merge queue config\n    if (userConfig.animation.queue) {\n      merged.animation.queue = {\n        ...DEFAULT_CONFIG.animation.queue,\n        ...userConfig.animation.queue\n      };\n    }\n\n    // Deep merge entry animation config\n    if (userConfig.animation.entry) {\n      merged.animation.entry = {\n        ...DEFAULT_CONFIG.animation.entry!,\n        ...userConfig.animation.entry,\n        start: userConfig.animation.entry.start\n          ? {\n              ...DEFAULT_CONFIG.animation.entry!.start,\n              ...userConfig.animation.entry.start,\n              circular: userConfig.animation.entry.start.circular\n                ? { ...DEFAULT_CONFIG.animation.entry!.start.circular, ...userConfig.animation.entry.start.circular }\n                : DEFAULT_CONFIG.animation.entry!.start.circular\n            }\n          : DEFAULT_CONFIG.animation.entry!.start,\n        timing: userConfig.animation.entry.timing\n          ? { ...DEFAULT_CONFIG.animation.entry!.timing, ...userConfig.animation.entry.timing }\n          : DEFAULT_CONFIG.animation.entry!.timing,\n        path: userConfig.animation.entry.path\n          ? { ...DEFAULT_PATH_CONFIG, ...userConfig.animation.entry.path }\n          : DEFAULT_CONFIG.animation.entry!.path,\n        rotation: userConfig.animation.entry.rotation\n          ? { ...DEFAULT_ENTRY_ROTATION, ...userConfig.animation.entry.rotation }\n          : DEFAULT_CONFIG.animation.entry!.rotation,\n        scale: userConfig.animation.entry.scale\n          ? { ...DEFAULT_ENTRY_SCALE, ...userConfig.animation.entry.scale }\n          : DEFAULT_CONFIG.animation.entry!.scale\n      };\n    }\n\n    // Deep merge idle animation config\n    if (userConfig.animation.idle) {\n      merged.animation.idle = {\n        ...DEFAULT_IDLE_CONFIG,\n        ...userConfig.animation.idle\n      };\n    }\n  }\n\n  // Deep merge interaction config\n  if (userConfig.interaction) {\n    merged.interaction = {\n      ...DEFAULT_CONFIG.interaction,\n      ...userConfig.interaction\n    } as any;\n\n    // Deep merge focus config\n    if (userConfig.interaction.focus) {\n      merged.interaction.focus = {\n        ...DEFAULT_CONFIG.interaction.focus,\n        ...userConfig.interaction.focus\n      };\n    }\n\n    // Deep merge navigation config\n    if (userConfig.interaction.navigation) {\n      merged.interaction.navigation = {\n        ...DEFAULT_CONFIG.interaction.navigation,\n        ...userConfig.interaction.navigation\n      };\n    }\n\n  }\n\n  // Merge ui config (with backwards compat for old rendering.ui)\n  const legacyUi = (userConfig as any).rendering?.ui;\n  if (legacyUi) {\n    console.warn('[ImageCloud] rendering.ui is deprecated. Use top-level ui instead.');\n  }\n  merged.ui = {\n    ...DEFAULT_CONFIG.ui,\n    ...legacyUi,\n    ...userConfig.ui\n  };\n\n  // Merge debug config\n  merged.config.debug = {\n    ...DEFAULT_DEBUG_CONFIG,\n    ...(userConfig.config?.debug ?? {})\n  };\n\n  // Honeycomb forces default/hover clip path to hexagon height-relative for edge-to-edge tiling\n  if (merged.layout.algorithm === 'honeycomb' && merged.styling) {\n    const honeycombClip = { shape: 'hexagon' as const, mode: 'height-relative' as const };\n    merged.styling = {\n      ...merged.styling,\n      default: { ...merged.styling.default, clipPath: honeycombClip },\n      hover:   { ...merged.styling.hover,   clipPath: honeycombClip },\n      // focused: untouched — user config respected\n    };\n  }\n\n  return merged;\n}\n\n/**\n * Resolve bounce path config from preset and overrides\n */\nexport function resolveBounceConfig(\n  preset?: BouncePreset,\n  overrides?: Partial<BouncePathConfig>\n): BouncePathConfig {\n  const base = preset ? BOUNCE_PRESETS[preset] : BOUNCE_PRESETS.playful;\n  return { ...base, ...overrides };\n}\n\n/**\n * Resolve elastic path config from preset and overrides\n */\nexport function resolveElasticConfig(\n  preset?: ElasticPreset,\n  overrides?: Partial<ElasticPathConfig>\n): ElasticPathConfig {\n  const base = preset ? ELASTIC_PRESETS[preset] : ELASTIC_PRESETS.gentle;\n  return { ...base, ...overrides };\n}\n\n/**\n * Resolve wave path config from preset and overrides\n */\nexport function resolveWavePathConfig(\n  preset?: WavePathPreset,\n  overrides?: Partial<WavePathConfig>\n): WavePathConfig {\n  const base = preset ? WAVE_PATH_PRESETS[preset] : WAVE_PATH_PRESETS.gentle;\n  return { ...base, ...overrides };\n}\n\n/**\n * Debug logger\n */\nexport function debugLog(config: ImageCloudConfig, ...args: unknown[]): void {\n  if (config.config.debug?.enabled && typeof console !== 'undefined') {\n    console.log(...args);\n  }\n}","/**\n * AnimationEngine.ts\n * Handles smooth animations with easing for the image cloud\n *\n * Public API:\n * - animateTransform(element, properties, duration, easing)\n * - animateTransformCancellable(element, from, to, duration, easing) - Web Animations API\n * - cancelAnimation(handle, commitStyle) - Cancel and optionally keep current position\n * - getCurrentTransform(element) - Get current transform state mid-animation\n * - hasActiveAnimation(element) - Check if element has active animation\n * - resetTransform(element, originalState)\n * - clearTransition(element)\n * - wait(ms)\n */\n\nimport type { AnimationConfig, TransformParams, ImageLayout, AnimationHandle, AnimationSnapshot } from '../config/types';\n\nexport class AnimationEngine {\n  private config: AnimationConfig;\n  private activeAnimations: Map<HTMLElement, AnimationHandle> = new Map();\n  private animationIdCounter = 0;\n\n  constructor(config: AnimationConfig) {\n    this.config = config;\n  }\n\n  /**\n   * Build transform string from transform params\n   * Always starts with centering transform to match image positioning system\n   */\n  private buildTransformString(params: TransformParams): string {\n    const transforms: string[] = ['translate(-50%, -50%)'];\n\n    if (params.x !== undefined || params.y !== undefined) {\n      const x = params.x ?? 0;\n      const y = params.y ?? 0;\n      transforms.push(`translate(${x}px, ${y}px)`);\n    }\n\n    if (params.rotation !== undefined) {\n      transforms.push(`rotate(${params.rotation}deg)`);\n    }\n\n    if (params.scale !== undefined) {\n      transforms.push(`scale(${params.scale})`);\n    }\n\n    return transforms.join(' ');\n  }\n\n  /**\n   * Start a cancellable transform animation using Web Animations API\n   * @param element - The element to animate\n   * @param from - Starting transform state\n   * @param to - Ending transform state\n   * @param duration - Animation duration in ms (optional)\n   * @param easing - CSS easing function (optional)\n   * @returns AnimationHandle that can be used to cancel or query the animation\n   */\n  animateTransformCancellable(\n    element: HTMLElement,\n    from: TransformParams,\n    to: TransformParams,\n    duration: number | null = null,\n    easing: string | null = null\n  ): AnimationHandle {\n    // Cancel any existing animation on this element\n    this.cancelAllAnimations(element);\n\n    const animDuration = duration ?? this.config.duration;\n    const animEasing = easing ?? this.config.easing.default;\n\n    const fromTransform = this.buildTransformString(from);\n    const toTransform = this.buildTransformString(to);\n\n    // Clear any CSS transitions to avoid conflicts\n    element.style.transition = 'none';\n\n    // Create Web Animation\n    const animation = element.animate(\n      [\n        { transform: fromTransform },\n        { transform: toTransform }\n      ],\n      {\n        duration: animDuration,\n        easing: animEasing,\n        fill: 'forwards'  // Keep final state after animation\n      }\n    );\n\n    const handle: AnimationHandle = {\n      id: `anim-${++this.animationIdCounter}`,\n      element,\n      animation,\n      fromState: from,\n      toState: to,\n      startTime: performance.now(),\n      duration: animDuration\n    };\n\n    this.activeAnimations.set(element, handle);\n\n    // Clean up when animation finishes (normally or cancelled)\n    animation.finished\n      .then(() => {\n        // Apply final transform as inline style for consistency\n        element.style.transform = toTransform;\n        this.activeAnimations.delete(element);\n      })\n      .catch(() => {\n        // Animation was cancelled - cleanup handled by cancelAnimation\n        this.activeAnimations.delete(element);\n      });\n\n    return handle;\n  }\n\n  /**\n   * Cancel an active animation\n   * @param handle - The animation handle to cancel\n   * @param commitStyle - If true, keeps current position; if false, no style change\n   * @returns Snapshot of where the animation was when cancelled\n   */\n  cancelAnimation(handle: AnimationHandle, commitStyle: boolean = true): AnimationSnapshot {\n    const snapshot = this.getCurrentTransform(handle.element);\n\n    // Cancel the Web Animation\n    handle.animation.cancel();\n\n    if (commitStyle) {\n      // Apply current position as inline style\n      const currentTransform = this.buildTransformString({\n        x: snapshot.x,\n        y: snapshot.y,\n        rotation: snapshot.rotation,\n        scale: snapshot.scale\n      });\n      handle.element.style.transform = currentTransform;\n    }\n\n    this.activeAnimations.delete(handle.element);\n\n    return snapshot;\n  }\n\n  /**\n   * Cancel all animations on an element\n   * Uses Web Animations API to find and cancel ALL animations, not just tracked ones\n   * @param element - The element to cancel animations for\n   */\n  cancelAllAnimations(element: HTMLElement): void {\n    // Cancel tracked animation\n    const handle = this.activeAnimations.get(element);\n    if (handle) {\n      this.cancelAnimation(handle, false);\n    }\n\n    // Also cancel any other animations on the element (e.g., completed animations with fill: 'forwards')\n    const allAnimations = element.getAnimations();\n    for (const anim of allAnimations) {\n      anim.cancel();\n    }\n  }\n\n  /**\n   * Get current transform state of an element (works mid-animation)\n   * Uses DOMMatrix to parse the computed transform\n   * @param element - The element to query\n   * @returns Current transform snapshot\n   */\n  getCurrentTransform(element: HTMLElement): AnimationSnapshot {\n    const computed = getComputedStyle(element);\n    const transformStr = computed.transform;\n\n    if (transformStr === 'none' || !transformStr) {\n      return { x: 0, y: 0, rotation: 0, scale: 1 };\n    }\n\n    const matrix = new DOMMatrix(transformStr);\n\n    // Extract scale from matrix (for uniform scale)\n    const scale = Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b);\n\n    // Extract rotation from matrix (in degrees)\n    const rotation = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);\n\n    // Extract translation\n    // Note: matrix.e and matrix.f include ALL translations including the centering offset\n    // The centering is translate(-50%, -50%) which depends on element dimensions\n    // Since our transform chain is: translate(-50%, -50%) translate(x, y) rotate scale\n    // The additional x,y offset we applied is already baked into e,f along with centering\n    // For cross-animation, we need the relative offset, not absolute position\n    // We'll return e,f directly and let ZoomEngine handle the interpretation\n    const x = matrix.e;\n    const y = matrix.f;\n\n    return { x, y, rotation, scale };\n  }\n\n  /**\n   * Check if an element has an active animation\n   * @param element - The element to check\n   * @returns True if animation is in progress\n   */\n  hasActiveAnimation(element: HTMLElement): boolean {\n    return this.activeAnimations.has(element);\n  }\n\n  /**\n   * Get animation handle for an element if it exists\n   * @param element - The element to query\n   * @returns AnimationHandle or undefined\n   */\n  getAnimationHandle(element: HTMLElement): AnimationHandle | undefined {\n    return this.activeAnimations.get(element);\n  }\n\n  /**\n   * Animate element transform with smooth easing (CSS transitions - legacy method)\n   * @param element - The element to animate\n   * @param properties - Transform properties {x, y, rotation, scale}\n   * @param duration - Animation duration in ms (optional)\n   * @param easing - CSS easing function (optional)\n   * @returns Promise that resolves when animation completes\n   */\n  animateTransform(\n    element: HTMLElement,\n    properties: TransformParams,\n    duration: number | null = null,\n    easing: string | null = null\n  ): Promise<void> {\n    return new Promise((resolve) => {\n      const animDuration = duration ?? this.config.duration;\n      const animEasing = easing ?? this.config.easing.default;\n\n      // Apply transition\n      element.style.transition = `transform ${animDuration}ms ${animEasing}, box-shadow ${animDuration}ms ${animEasing}`;\n\n      // Apply transform using shared helper\n      element.style.transform = this.buildTransformString(properties);\n\n      // Resolve promise when animation completes\n      setTimeout(() => {\n        resolve();\n      }, animDuration);\n    });\n  }\n\n  /**\n   * Reset element to its original transform\n   * @param element - The element to reset\n   * @param originalState - Original transform state {x, y, rotation, scale}\n   * @returns Promise that resolves when animation completes\n   */\n  resetTransform(element: HTMLElement, originalState: TransformParams | ImageLayout): Promise<void> {\n    return this.animateTransform(element, originalState);\n  }\n\n  /**\n   * Remove transition styles from element\n   * @param element - The element to clear\n   */\n  clearTransition(element: HTMLElement): void {\n    element.style.transition = '';\n  }\n\n  /**\n   * Utility: Wait for a specified duration\n   * @param ms - Milliseconds to wait\n   * @returns Promise that resolves after the specified duration\n   */\n  wait(ms: number): Promise<void> {\n    return new Promise(resolve => setTimeout(resolve, ms));\n  }\n}","/**\n * PathAnimator.ts\n * Provides path calculation functions for bounce, elastic, and wave entry animations\n *\n * These animations require JavaScript-driven frame updates because they involve\n * complex mathematical curves that can't be expressed with CSS transitions alone.\n *\n * Public API:\n * - animatePath(element, startPos, endPos, pathConfig, duration, onComplete)\n * - calculateBouncePosition(t, start, end, config)\n * - calculateElasticPosition(t, start, end, config)\n * - calculateWavePosition(t, start, end, config)\n */\n\nimport type {\n  BouncePathConfig,\n  ElasticPathConfig,\n  WavePathConfig,\n  EntryPathConfig,\n  EntryPathType,\n  EntryRotationConfig,\n  EntryScaleConfig\n} from '../config/types';\nimport {\n  resolveBounceConfig,\n  resolveElasticConfig,\n  resolveWavePathConfig\n} from '../config/defaults';\n\nexport interface Point {\n  x: number;\n  y: number;\n}\n\nexport interface PathAnimationOptions {\n  element: HTMLElement;\n  startPosition: Point;\n  endPosition: Point;\n  pathConfig: EntryPathConfig;\n  duration: number;\n  imageWidth: number;\n  imageHeight: number;\n  rotation: number;           // Final rotation\n  scale: number;              // Final scale\n  onComplete?: () => void;\n  // Rotation animation options\n  rotationConfig?: EntryRotationConfig;\n  startRotation?: number;     // Starting rotation (if different from final)\n  // Scale animation options\n  scaleConfig?: EntryScaleConfig;\n  startScale?: number;        // Starting scale (if different from final)\n}\n\n/**\n * Linear interpolation helper\n */\nfunction lerp(start: number, end: number, t: number): number {\n  return start + (end - start) * t;\n}\n\n/**\n * Calculate position along a bounce path\n * Overshoot and settle animation\n */\nexport function calculateBouncePosition(\n  t: number,\n  start: Point,\n  end: Point,\n  config: BouncePathConfig\n): Point {\n  const { overshoot, bounces, decayRatio } = config;\n\n  // Direction vector from start to end\n  const dx = end.x - start.x;\n  const dy = end.y - start.y;\n\n  // Calculate keyframe timings based on number of bounces\n  const keyframes = generateBounceKeyframes(bounces, decayRatio);\n\n  // Find current segment\n  let progress = 0;\n  let segmentStart = 0;\n  let segmentEnd = 1;\n  let segmentOvershoot = overshoot;\n  let isOvershootPhase = false;\n\n  for (let i = 0; i < keyframes.length; i++) {\n    if (t <= keyframes[i].time) {\n      segmentStart = i === 0 ? 0 : keyframes[i - 1].time;\n      segmentEnd = keyframes[i].time;\n      segmentOvershoot = keyframes[i].overshoot;\n      isOvershootPhase = keyframes[i].isOvershoot;\n      break;\n    }\n  }\n\n  // Calculate progress within current segment\n  const segmentT = (t - segmentStart) / (segmentEnd - segmentStart);\n\n  if (isOvershootPhase) {\n    // Ease out into overshoot\n    progress = 1 + segmentOvershoot * easeOutQuad(segmentT);\n  } else if (segmentStart === 0) {\n    // Initial travel to target - ease out\n    progress = easeOutQuad(segmentT);\n  } else {\n    // Settling back from overshoot\n    const prevOvershoot = keyframes.find((k, i) =>\n      k.time > segmentStart && i > 0 && keyframes[i - 1].isOvershoot\n    );\n    const fromProgress = 1 + (prevOvershoot?.overshoot || segmentOvershoot);\n    progress = lerp(fromProgress, 1, easeOutQuad(segmentT));\n  }\n\n  return {\n    x: start.x + dx * progress,\n    y: start.y + dy * progress\n  };\n}\n\n/**\n * Generate keyframe timings for bounce animation\n */\nfunction generateBounceKeyframes(\n  bounces: number,\n  decayRatio: number\n): Array<{ time: number; overshoot: number; isOvershoot: boolean }> {\n  const keyframes: Array<{ time: number; overshoot: number; isOvershoot: boolean }> = [];\n\n  // Initial travel takes 60% of time\n  let currentTime = 0.6;\n  keyframes.push({ time: currentTime, overshoot: 0, isOvershoot: false });\n\n  let currentOvershoot = 0.15; // Initial overshoot amount\n  const remainingTime = 0.4;\n  const bounceTime = remainingTime / (bounces * 2);\n\n  for (let i = 0; i < bounces; i++) {\n    // Overshoot phase\n    currentTime += bounceTime;\n    keyframes.push({ time: currentTime, overshoot: currentOvershoot, isOvershoot: true });\n\n    // Settle phase\n    currentTime += bounceTime;\n    keyframes.push({ time: currentTime, overshoot: currentOvershoot * decayRatio, isOvershoot: false });\n\n    currentOvershoot *= decayRatio;\n  }\n\n  // Final settle\n  keyframes.push({ time: 1, overshoot: 0, isOvershoot: false });\n\n  return keyframes;\n}\n\n/**\n * Calculate position along an elastic path\n * Spring-like oscillation animation\n */\nexport function calculateElasticPosition(\n  t: number,\n  start: Point,\n  end: Point,\n  config: ElasticPathConfig\n): Point {\n  const { stiffness, damping, mass, oscillations } = config;\n\n  // Direction vector from start to end\n  const dx = end.x - start.x;\n  const dy = end.y - start.y;\n\n  // Normalized spring physics\n  // Natural frequency based on stiffness and mass\n  const omega = Math.sqrt(stiffness / mass);\n\n  // Damping ratio (normalized)\n  const zeta = damping / (2 * Math.sqrt(stiffness * mass));\n\n  // Progress using damped harmonic oscillator\n  let progress: number;\n\n  if (zeta < 1) {\n    // Underdamped - oscillates\n    const dampedFreq = omega * Math.sqrt(1 - zeta * zeta);\n    const envelope = Math.exp(-zeta * omega * t * 3);\n    const oscillation = Math.cos(dampedFreq * t * oscillations * Math.PI);\n    progress = 1 - envelope * oscillation;\n  } else {\n    // Critically damped or overdamped - no oscillation\n    progress = 1 - Math.exp(-omega * t * 3);\n  }\n\n  // Clamp progress\n  progress = Math.max(0, Math.min(progress, 1.3)); // Allow slight overshoot\n\n  return {\n    x: start.x + dx * progress,\n    y: start.y + dy * progress\n  };\n}\n\n/**\n * Calculate position along a wave path\n * Sinusoidal path from start to end\n */\nexport function calculateWavePosition(\n  t: number,\n  start: Point,\n  end: Point,\n  config: WavePathConfig\n): Point {\n  const { amplitude, frequency, decay, decayRate, phase } = config;\n\n  // Direction vector from start to end\n  const dx = end.x - start.x;\n  const dy = end.y - start.y;\n  const length = Math.sqrt(dx * dx + dy * dy);\n\n  // Perpendicular vector (normalized)\n  const perpX = length > 0 ? -dy / length : 0;\n  const perpY = length > 0 ? dx / length : 1;\n\n  // Wave calculation\n  const wavePhase = frequency * Math.PI * 2 * t + phase;\n  const decayFactor = decay ? Math.pow(1 - t, decayRate) : 1;\n  const waveOffset = amplitude * Math.sin(wavePhase) * decayFactor;\n\n  // Ease out for smooth arrival\n  const progressT = easeOutCubic(t);\n\n  // Linear interpolation + wave offset\n  return {\n    x: lerp(start.x, end.x, progressT) + waveOffset * perpX,\n    y: lerp(start.y, end.y, progressT) + waveOffset * perpY\n  };\n}\n\n/**\n * Easing functions\n */\nfunction easeOutQuad(t: number): number {\n  return 1 - (1 - t) * (1 - t);\n}\n\nfunction easeOutCubic(t: number): number {\n  return 1 - Math.pow(1 - t, 3);\n}\n\n/**\n * Calculate wobble rotation for a given animation progress\n */\nfunction calculateWobbleRotation(\n  progress: number,\n  finalRotation: number,\n  wobbleConfig: { amplitude: number; frequency: number; decay: boolean }\n): number {\n  const { amplitude, frequency, decay } = wobbleConfig;\n\n  // Oscillation using sine wave\n  const oscillation = Math.sin(progress * frequency * Math.PI * 2);\n\n  // Apply decay if enabled (stronger decay toward end)\n  const decayFactor = decay ? Math.pow(1 - progress, 2) : 1;\n\n  // Calculate wobble offset\n  const wobbleOffset = amplitude * oscillation * decayFactor;\n\n  return finalRotation + wobbleOffset;\n}\n\n/**\n * Calculate pop scale for a given animation progress\n */\nfunction calculatePopScale(\n  progress: number,\n  finalScale: number,\n  popConfig: { overshoot: number; bounces: number }\n): number {\n  const { overshoot, bounces } = popConfig;\n\n  // Generate keyframes for bounce effect\n  const keyframes: Array<{ time: number; scale: number }> = [];\n\n  // Reach overshoot at 50% of animation\n  keyframes.push({ time: 0.5, scale: overshoot });\n\n  // Add bounces\n  let currentOvershoot = overshoot;\n  const bounceDecay = 0.5;\n  const remainingTime = 0.5;\n  const bounceTime = remainingTime / (bounces * 2);\n\n  let currentTime = 0.5;\n  for (let i = 0; i < bounces; i++) {\n    const undershoot = 1 - (currentOvershoot - 1) * bounceDecay;\n    currentTime += bounceTime;\n    keyframes.push({ time: currentTime, scale: undershoot });\n\n    currentOvershoot = 1 + (currentOvershoot - 1) * bounceDecay * bounceDecay;\n    currentTime += bounceTime;\n    if (i < bounces - 1) {\n      keyframes.push({ time: currentTime, scale: currentOvershoot });\n    }\n  }\n\n  keyframes.push({ time: 1, scale: 1 });\n\n  // Find current segment\n  let currentScale = 1;\n  for (let i = 0; i < keyframes.length; i++) {\n    if (progress <= keyframes[i].time) {\n      const prevTime = i === 0 ? 0 : keyframes[i - 1].time;\n      const prevScale = i === 0 ? 1 : keyframes[i - 1].scale;\n      const segmentProgress = (progress - prevTime) / (keyframes[i].time - prevTime);\n      const easedProgress = easeOutQuad(segmentProgress);\n      currentScale = prevScale + (keyframes[i].scale - prevScale) * easedProgress;\n      break;\n    }\n  }\n\n  return currentScale * finalScale;\n}\n\n/**\n * Animate an element along a path using requestAnimationFrame\n */\nexport function animatePath(options: PathAnimationOptions): void {\n  const {\n    element,\n    startPosition,\n    endPosition,\n    pathConfig,\n    duration,\n    imageWidth,\n    imageHeight,\n    rotation: finalRotation,\n    scale: finalScale,\n    onComplete,\n    rotationConfig,\n    startRotation,\n    scaleConfig,\n    startScale\n  } = options;\n\n  const pathType = pathConfig.type;\n\n  // Determine if we need to animate rotation\n  const animateRotation = startRotation !== undefined && startRotation !== finalRotation;\n  const isWobbleMode = rotationConfig?.mode === 'wobble';\n  const wobbleConfig = rotationConfig?.wobble || { amplitude: 15, frequency: 3, decay: true };\n  const needsRotationAnimation = animateRotation || isWobbleMode;\n\n  // Determine if we need to animate scale\n  const animateScale = startScale !== undefined && startScale !== finalScale;\n  const isPopMode = scaleConfig?.mode === 'pop';\n  const popConfig = scaleConfig?.pop || { overshoot: 1.2, bounces: 1 };\n  const needsScaleAnimation = animateScale || isPopMode;\n\n  // For linear/arc paths WITHOUT rotation or scale animation, use CSS transitions (handled elsewhere)\n  if ((pathType === 'linear' || pathType === 'arc') && !needsRotationAnimation && !needsScaleAnimation) {\n    if (onComplete) onComplete();\n    return;\n  }\n\n  const startTime = performance.now();\n\n  // Build center offset for transform\n  const centerOffsetX = -imageWidth / 2;\n  const centerOffsetY = -imageHeight / 2;\n\n  function tick(currentTime: number): void {\n    const elapsed = currentTime - startTime;\n    const t = Math.min(elapsed / duration, 1);\n\n    // Calculate position based on path type\n    let position: Point;\n\n    switch (pathType) {\n      case 'bounce': {\n        const config = resolveBounceConfig(\n          pathConfig.bouncePreset,\n          pathConfig.bounce\n        );\n        position = calculateBouncePosition(t, startPosition, endPosition, config);\n        break;\n      }\n      case 'elastic': {\n        const config = resolveElasticConfig(\n          pathConfig.elasticPreset,\n          pathConfig.elastic\n        );\n        position = calculateElasticPosition(t, startPosition, endPosition, config);\n        break;\n      }\n      case 'wave': {\n        const config = resolveWavePathConfig(\n          pathConfig.wavePreset,\n          pathConfig.wave\n        );\n        position = calculateWavePosition(t, startPosition, endPosition, config);\n        break;\n      }\n      default:\n        position = {\n          x: lerp(startPosition.x, endPosition.x, t),\n          y: lerp(startPosition.y, endPosition.y, t)\n        };\n    }\n\n    // Calculate translate offset from final position\n    const translateX = position.x - endPosition.x;\n    const translateY = position.y - endPosition.y;\n\n    // Calculate current rotation\n    let currentRotation: number;\n    if (isWobbleMode) {\n      currentRotation = calculateWobbleRotation(t, finalRotation, wobbleConfig);\n    } else if (animateRotation) {\n      currentRotation = lerp(startRotation!, finalRotation, t);\n    } else {\n      currentRotation = finalRotation;\n    }\n\n    // Calculate current scale\n    let currentScale: number;\n    if (isPopMode) {\n      currentScale = calculatePopScale(t, finalScale, popConfig);\n    } else if (animateScale) {\n      currentScale = lerp(startScale!, finalScale, t);\n    } else {\n      currentScale = finalScale;\n    }\n\n    // Apply transform\n    element.style.transform =\n      `translate(${centerOffsetX}px, ${centerOffsetY}px) ` +\n      `translate(${translateX}px, ${translateY}px) ` +\n      `rotate(${currentRotation}deg) scale(${currentScale})`;\n\n    if (t < 1) {\n      requestAnimationFrame(tick);\n    } else {\n      // Ensure we end exactly at the final position, rotation, and scale\n      element.style.transform =\n        `translate(${centerOffsetX}px, ${centerOffsetY}px) ` +\n        `rotate(${finalRotation}deg) scale(${finalScale})`;\n      if (onComplete) onComplete();\n    }\n  }\n\n  requestAnimationFrame(tick);\n}\n\n/**\n * Check if a path type requires JavaScript animation (vs CSS transitions)\n */\nexport function requiresJSAnimation(pathType: EntryPathType): boolean {\n  return pathType === 'bounce' || pathType === 'elastic' || pathType === 'wave';\n}\n\n","/**\n * EntryAnimationEngine.ts\n * Calculates starting positions for entry animations based on configuration\n *\n * Public API:\n * - calculateStartPosition(finalPosition, imageSize, containerBounds, imageIndex, totalImages)\n * - getAnimationParams(imageIndex)\n */\n\nimport type {\n  EntryAnimationConfig,\n  EntryStartPosition,\n  ContainerBounds,\n  LayoutAlgorithm,\n  EntryPathConfig,\n  EntryPathType,\n  EntryRotationConfig,\n  EntryRotationMode,\n  EntryScaleConfig,\n  EntryScaleMode\n} from '../config/types';\nimport { DEFAULT_PATH_CONFIG, DEFAULT_ENTRY_ROTATION, DEFAULT_ENTRY_SCALE } from '../config/defaults';\nimport { requiresJSAnimation } from './PathAnimator';\n\n/** Layout-aware default start positions */\nconst LAYOUT_ENTRY_DEFAULTS: Record<LayoutAlgorithm, EntryStartPosition> = {\n  radial: 'center',\n  spiral: 'center',\n  grid: 'top',\n  cluster: 'nearest-edge',\n  random: 'nearest-edge',\n  wave: 'left',\n  honeycomb: 'center'\n};\n\nexport interface StartPosition {\n  x: number;\n  y: number;\n  useScale?: boolean;  // For center position, start with scale 0\n}\n\nexport interface AnimationParams {\n  startTransform: string;\n  duration: number;\n  delay: number;\n  easing: string;\n}\n\nexport class EntryAnimationEngine {\n  private config: EntryAnimationConfig;\n  private layoutAlgorithm: LayoutAlgorithm;\n  private resolvedStartPosition: EntryStartPosition;\n  private pathConfig: EntryPathConfig;\n  private rotationConfig: EntryRotationConfig;\n  private scaleConfig: EntryScaleConfig;\n\n  constructor(config: EntryAnimationConfig, layoutAlgorithm: LayoutAlgorithm) {\n    this.config = config;\n    this.layoutAlgorithm = layoutAlgorithm;\n\n    // Resolve the start position, using layout-aware defaults if not specified\n    this.resolvedStartPosition = this.resolveStartPosition();\n\n    // Resolve path config\n    this.pathConfig = config.path || DEFAULT_PATH_CONFIG;\n\n    // Resolve rotation config\n    this.rotationConfig = config.rotation || DEFAULT_ENTRY_ROTATION;\n\n    // Resolve scale config\n    this.scaleConfig = config.scale || DEFAULT_ENTRY_SCALE;\n  }\n\n  /**\n   * Get the effective start position, considering layout-aware defaults\n   */\n  private resolveStartPosition(): EntryStartPosition {\n    // If explicitly configured, use that\n    if (this.config.start.position) {\n      return this.config.start.position;\n    }\n    // Otherwise use layout-aware default\n    return LAYOUT_ENTRY_DEFAULTS[this.layoutAlgorithm] || 'nearest-edge';\n  }\n\n  /**\n   * Calculate the starting position for an image's entry animation\n   */\n  calculateStartPosition(\n    finalPosition: { x: number; y: number },\n    imageSize: { width: number; height: number },\n    containerBounds: ContainerBounds,\n    imageIndex: number,\n    totalImages: number\n  ): StartPosition {\n    const position = this.resolvedStartPosition;\n    const offset = this.config.start.offset ?? 100;\n\n    switch (position) {\n      case 'nearest-edge':\n        return this.calculateNearestEdge(finalPosition, imageSize, containerBounds, offset);\n\n      case 'top':\n        return this.calculateEdgePosition('top', finalPosition, imageSize, containerBounds, offset);\n\n      case 'bottom':\n        return this.calculateEdgePosition('bottom', finalPosition, imageSize, containerBounds, offset);\n\n      case 'left':\n        return this.calculateEdgePosition('left', finalPosition, imageSize, containerBounds, offset);\n\n      case 'right':\n        return this.calculateEdgePosition('right', finalPosition, imageSize, containerBounds, offset);\n\n      case 'center':\n        return this.calculateCenterPosition(containerBounds, finalPosition, imageSize);\n\n      case 'random-edge':\n        return this.calculateRandomEdge(finalPosition, imageSize, containerBounds, offset);\n\n      case 'circular':\n        return this.calculateCircularPosition(\n          finalPosition,\n          imageSize,\n          containerBounds,\n          imageIndex,\n          totalImages\n        );\n\n      default:\n        return this.calculateNearestEdge(finalPosition, imageSize, containerBounds, offset);\n    }\n  }\n\n  /**\n   * Calculate start position from the nearest edge (current default behavior)\n   */\n  private calculateNearestEdge(\n    finalPosition: { x: number; y: number },\n    imageSize: { width: number; height: number },\n    containerBounds: ContainerBounds,\n    offset: number\n  ): StartPosition {\n    // finalPosition now stores center position directly\n    const centerX = finalPosition.x;\n    const centerY = finalPosition.y;\n\n    const distLeft = centerX;\n    const distRight = containerBounds.width - centerX;\n    const distTop = centerY;\n    const distBottom = containerBounds.height - centerY;\n\n    const minDist = Math.min(distLeft, distRight, distTop, distBottom);\n\n    let startX = finalPosition.x;\n    let startY = finalPosition.y;\n\n    if (minDist === distLeft) {\n      // Start from left edge\n      startX = -(imageSize.width + offset);\n    } else if (minDist === distRight) {\n      // Start from right edge\n      startX = containerBounds.width + offset;\n    } else if (minDist === distTop) {\n      // Start from top edge\n      startY = -(imageSize.height + offset);\n    } else {\n      // Start from bottom edge\n      startY = containerBounds.height + offset;\n    }\n\n    return { x: startX, y: startY };\n  }\n\n  /**\n   * Calculate start position from a specific edge\n   */\n  private calculateEdgePosition(\n    edge: 'top' | 'bottom' | 'left' | 'right',\n    finalPosition: { x: number; y: number },\n    imageSize: { width: number; height: number },\n    containerBounds: ContainerBounds,\n    offset: number\n  ): StartPosition {\n    let startX = finalPosition.x;\n    let startY = finalPosition.y;\n\n    switch (edge) {\n      case 'top':\n        startY = -(imageSize.height + offset);\n        break;\n      case 'bottom':\n        startY = containerBounds.height + offset;\n        break;\n      case 'left':\n        startX = -(imageSize.width + offset);\n        break;\n      case 'right':\n        startX = containerBounds.width + offset;\n        break;\n    }\n\n    return { x: startX, y: startY };\n  }\n\n  /**\n   * Calculate start position from center with scale animation\n   */\n  private calculateCenterPosition(\n    containerBounds: ContainerBounds,\n    _finalPosition: { x: number; y: number },\n    _imageSize: { width: number; height: number }\n  ): StartPosition {\n    // Start at center of container (using center position, not top-left)\n    const centerX = containerBounds.width / 2;\n    const centerY = containerBounds.height / 2;\n\n    return {\n      x: centerX,\n      y: centerY,\n      useScale: true  // Signal to use scale animation from 0\n    };\n  }\n\n  /**\n   * Calculate start position from a random edge\n   */\n  private calculateRandomEdge(\n    finalPosition: { x: number; y: number },\n    imageSize: { width: number; height: number },\n    containerBounds: ContainerBounds,\n    offset: number\n  ): StartPosition {\n    const edges: ('top' | 'bottom' | 'left' | 'right')[] = ['top', 'bottom', 'left', 'right'];\n    const randomEdge = edges[Math.floor(Math.random() * edges.length)];\n    return this.calculateEdgePosition(randomEdge, finalPosition, imageSize, containerBounds, offset);\n  }\n\n  /**\n   * Calculate start position on a circle around the container\n   */\n  private calculateCircularPosition(\n    _finalPosition: { x: number; y: number },\n    _imageSize: { width: number; height: number },\n    containerBounds: ContainerBounds,\n    imageIndex: number,\n    totalImages: number\n  ): StartPosition {\n    const circularConfig = this.config.start.circular || {};\n    const distribution = circularConfig.distribution || 'even';\n\n    // Calculate radius\n    let radius: number;\n    const radiusConfig = circularConfig.radius || '120%';\n\n    if (typeof radiusConfig === 'string' && radiusConfig.endsWith('%')) {\n      // Percentage of container diagonal\n      const percentage = parseFloat(radiusConfig) / 100;\n      const diagonal = Math.sqrt(\n        containerBounds.width ** 2 + containerBounds.height ** 2\n      );\n      radius = diagonal * percentage / 2;\n    } else {\n      radius = typeof radiusConfig === 'number' ? radiusConfig : 500;\n    }\n\n    // Calculate angle\n    let angle: number;\n    if (distribution === 'even') {\n      angle = (imageIndex / totalImages) * 2 * Math.PI;\n    } else {\n      angle = Math.random() * 2 * Math.PI;\n    }\n\n    // Calculate position on circle, centered on container\n    const centerX = containerBounds.width / 2;\n    const centerY = containerBounds.height / 2;\n\n    // Return center position on circle (not top-left)\n    const startX = centerX + Math.cos(angle) * radius;\n    const startY = centerY + Math.sin(angle) * radius;\n\n    return { x: startX, y: startY };\n  }\n\n  /**\n   * Get animation parameters for an image\n   */\n  getAnimationParams(_imageIndex: number): AnimationParams {\n    const duration = this.config.timing.duration;\n    const easing = this.config.easing;\n\n    return {\n      startTransform: '',  // Will be computed by caller based on start position\n      duration,\n      delay: 0,\n      easing\n    };\n  }\n\n  /**\n   * Build a CSS transform string for the start position\n   * Uses pixel-based centering offset for reliable cross-browser behavior\n   */\n  buildStartTransform(\n    startPosition: StartPosition,\n    finalPosition: { x: number; y: number },\n    finalRotation: number,\n    finalScale: number,\n    imageWidth?: number,\n    imageHeight?: number,\n    startRotation?: number,\n    startScale?: number\n  ): string {\n    // Calculate translation from final to start position\n    const translateX = startPosition.x - finalPosition.x;\n    const translateY = startPosition.y - finalPosition.y;\n\n    // Use start rotation if provided, otherwise use final rotation\n    const rotation = startRotation !== undefined ? startRotation : finalRotation;\n\n    // Use start scale if provided, otherwise use final scale\n    const scale = startScale !== undefined ? startScale : finalScale;\n\n    // Use pixel offset if dimensions provided\n    const centerOffsetX = imageWidth !== undefined ? -imageWidth / 2 : 0;\n    const centerOffsetY = imageHeight !== undefined ? -imageHeight / 2 : 0;\n    const centerTranslate = imageWidth !== undefined\n      ? `translate(${centerOffsetX}px, ${centerOffsetY}px)`\n      : `translate(-50%, -50%)`;\n\n    if (startPosition.useScale) {\n      // For center position: start at center with scale 0\n      return `${centerTranslate} translate(${translateX}px, ${translateY}px) rotate(${rotation}deg) scale(0)`;\n    }\n\n    // Standard entry: translate from edge, with configured start scale\n    return `${centerTranslate} translate(${translateX}px, ${translateY}px) rotate(${rotation}deg) scale(${scale})`;\n  }\n\n  /**\n   * Build the final CSS transform string\n   * Uses pixel-based centering offset for reliable cross-browser behavior\n   */\n  buildFinalTransform(rotation: number, scale: number, imageWidth?: number, imageHeight?: number): string {\n    // Use pixel offset if dimensions provided, otherwise fall back to percentage\n    if (imageWidth !== undefined && imageHeight !== undefined) {\n      const offsetX = -imageWidth / 2;\n      const offsetY = -imageHeight / 2;\n      return `translate(${offsetX}px, ${offsetY}px) rotate(${rotation}deg) scale(${scale})`;\n    }\n    return `translate(-50%, -50%) rotate(${rotation}deg) scale(${scale})`;\n  }\n\n  /**\n   * Get the transition CSS for entry animation\n   * For JS-animated paths, only animate opacity (transform handled by JS)\n   */\n  getTransitionCSS(): string {\n    const duration = this.config.timing.duration;\n    const easing = this.config.easing;\n\n    // For paths that use JS animation, don't transition transform\n    if (this.requiresJSAnimation()) {\n      return `opacity ${duration}ms ease-out`;\n    }\n\n    return `opacity ${duration}ms ease-out, transform ${duration}ms ${easing}`;\n  }\n\n  /**\n   * Check if the current path type requires JavaScript animation\n   */\n  requiresJSAnimation(): boolean {\n    return requiresJSAnimation(this.pathConfig.type);\n  }\n\n  /**\n   * Get the path configuration\n   */\n  getPathConfig(): EntryPathConfig {\n    return this.pathConfig;\n  }\n\n  /**\n   * Get the path type\n   */\n  getPathType(): EntryPathType {\n    return this.pathConfig.type;\n  }\n\n  /**\n   * Get animation timing configuration\n   */\n  getTiming(): { duration: number } {\n    return {\n      duration: this.config.timing.duration\n    };\n  }\n\n  /**\n   * Get the rotation configuration\n   */\n  getRotationConfig(): EntryRotationConfig {\n    return this.rotationConfig;\n  }\n\n  /**\n   * Get the rotation mode\n   */\n  getRotationMode(): EntryRotationMode {\n    return this.rotationConfig.mode;\n  }\n\n  /**\n   * Calculate the starting rotation for an entry animation\n   * @param finalRotation - The final rotation from the layout\n   * @returns The starting rotation in degrees\n   */\n  calculateStartRotation(finalRotation: number): number {\n    const mode = this.rotationConfig.mode;\n\n    switch (mode) {\n      case 'none':\n        // No rotation animation - start at final rotation\n        return finalRotation;\n\n      case 'settle': {\n        // Start at a configured rotation and settle to final\n        const startConfig = this.rotationConfig.startRotation;\n        if (startConfig === undefined) {\n          // Default: ±30° random offset from final\n          return finalRotation + (Math.random() - 0.5) * 60;\n        }\n        if (typeof startConfig === 'number') {\n          return startConfig;\n        }\n        // Range: random value between min and max\n        const range = startConfig.max - startConfig.min;\n        return startConfig.min + Math.random() * range;\n      }\n\n      case 'spin': {\n        // Spin from a rotated position to final\n        const spinCount = this.rotationConfig.spinCount ?? 1;\n        const direction = this.resolveSpinDirection(finalRotation);\n        return finalRotation + (spinCount * 360 * direction);\n      }\n\n      case 'random':\n        // Random starting rotation (±30° from final)\n        return finalRotation + (Math.random() - 0.5) * 60;\n\n      case 'wobble':\n        // Wobble is handled in JS animation, start at final rotation\n        return finalRotation;\n\n      default:\n        return finalRotation;\n    }\n  }\n\n  /**\n   * Resolve spin direction based on config\n   * @returns 1 for clockwise, -1 for counterclockwise\n   */\n  private resolveSpinDirection(finalRotation: number): number {\n    const direction = this.rotationConfig.direction ?? 'auto';\n\n    switch (direction) {\n      case 'clockwise':\n        return -1;  // Negative rotation = clockwise spin to final\n      case 'counterclockwise':\n        return 1;   // Positive rotation = counterclockwise spin to final\n      case 'random':\n        return Math.random() < 0.5 ? 1 : -1;\n      case 'auto':\n      default:\n        // Auto: choose direction that reduces total rotation distance\n        // If final rotation is positive, spin counterclockwise (add positive offset)\n        return finalRotation >= 0 ? 1 : -1;\n    }\n  }\n\n  /**\n   * Check if the current rotation mode requires JavaScript animation\n   * (as opposed to CSS transitions)\n   */\n  requiresJSRotation(): boolean {\n    return this.rotationConfig.mode === 'wobble';\n  }\n\n  /**\n   * Calculate wobble rotation for a given animation progress\n   * @param progress - Animation progress from 0 to 1\n   * @param finalRotation - The final rotation in degrees\n   * @returns The current rotation in degrees\n   */\n  calculateWobbleRotation(progress: number, finalRotation: number): number {\n    if (this.rotationConfig.mode !== 'wobble') {\n      return finalRotation;\n    }\n\n    const wobbleConfig = this.rotationConfig.wobble || {\n      amplitude: 15,\n      frequency: 3,\n      decay: true\n    };\n\n    const { amplitude, frequency, decay } = wobbleConfig;\n\n    // Oscillation using sine wave\n    const oscillation = Math.sin(progress * frequency * Math.PI * 2);\n\n    // Apply decay if enabled (stronger decay toward end)\n    const decayFactor = decay ? Math.pow(1 - progress, 2) : 1;\n\n    // Calculate wobble offset\n    const wobbleOffset = amplitude * oscillation * decayFactor;\n\n    return finalRotation + wobbleOffset;\n  }\n\n  /**\n   * Get the scale configuration\n   */\n  getScaleConfig(): EntryScaleConfig {\n    return this.scaleConfig;\n  }\n\n  /**\n   * Get the scale mode\n   */\n  getScaleMode(): EntryScaleMode {\n    return this.scaleConfig.mode;\n  }\n\n  /**\n   * Calculate the starting scale for an entry animation\n   * @param finalScale - The final scale from the layout\n   * @returns The starting scale\n   */\n  calculateStartScale(finalScale: number): number {\n    const mode = this.scaleConfig.mode;\n\n    switch (mode) {\n      case 'none':\n        // No scale animation - start at final scale\n        return finalScale;\n\n      case 'grow': {\n        // Start smaller, grow to final\n        const startScale = this.scaleConfig.startScale ?? 0.3;\n        return startScale * finalScale;  // Apply relative to final scale\n      }\n\n      case 'shrink': {\n        // Start larger, shrink to final\n        const startScale = this.scaleConfig.startScale ?? 1.5;\n        return startScale * finalScale;  // Apply relative to final scale\n      }\n\n      case 'pop':\n        // Pop mode uses JS animation, start at final scale\n        // (the overshoot/bounce is handled in the animation tick)\n        return finalScale;\n\n      case 'random': {\n        // Random start scale in configured range\n        const range = this.scaleConfig.range ?? { min: 0.5, max: 1.0 };\n        const randomFactor = range.min + Math.random() * (range.max - range.min);\n        return randomFactor * finalScale;\n      }\n\n      default:\n        return finalScale;\n    }\n  }\n\n  /**\n   * Check if the current scale mode requires JavaScript animation\n   * (as opposed to CSS transitions)\n   */\n  requiresJSScale(): boolean {\n    return this.scaleConfig.mode === 'pop';\n  }\n\n  /**\n   * Calculate pop scale for a given animation progress\n   * @param progress - Animation progress from 0 to 1\n   * @param finalScale - The final scale value\n   * @returns The current scale value with bounce effect\n   */\n  calculatePopScale(progress: number, finalScale: number): number {\n    if (this.scaleConfig.mode !== 'pop') {\n      return finalScale;\n    }\n\n    const popConfig = this.scaleConfig.pop || {\n      overshoot: 1.2,\n      bounces: 1\n    };\n\n    const { overshoot, bounces } = popConfig;\n\n    // Create keyframes for bounce effect\n    // Similar to bounce path but for scale\n    const keyframes = this.generateScaleBounceKeyframes(bounces, overshoot);\n\n    // Find current segment\n    let currentScale = finalScale;\n    for (let i = 0; i < keyframes.length; i++) {\n      if (progress <= keyframes[i].time) {\n        const prevTime = i === 0 ? 0 : keyframes[i - 1].time;\n        const prevScale = i === 0 ? finalScale : keyframes[i - 1].scale;\n        const segmentProgress = (progress - prevTime) / (keyframes[i].time - prevTime);\n        // Smooth easing within segment\n        const easedProgress = this.easeOutQuad(segmentProgress);\n        currentScale = prevScale + (keyframes[i].scale - prevScale) * easedProgress;\n        break;\n      }\n    }\n\n    return currentScale * finalScale;\n  }\n\n  /**\n   * Generate keyframes for scale bounce animation\n   */\n  private generateScaleBounceKeyframes(\n    bounces: number,\n    overshoot: number\n  ): Array<{ time: number; scale: number }> {\n    const keyframes: Array<{ time: number; scale: number }> = [];\n\n    // Reach overshoot at 50% of animation\n    keyframes.push({ time: 0.5, scale: overshoot });\n\n    // Add bounces\n    let currentOvershoot = overshoot;\n    const bounceDecay = 0.5;  // Each bounce is 50% of previous\n    const remainingTime = 0.5;\n    const bounceTime = remainingTime / (bounces * 2);\n\n    let currentTime = 0.5;\n    for (let i = 0; i < bounces; i++) {\n      // Undershoot (go below 1)\n      const undershoot = 1 - (currentOvershoot - 1) * bounceDecay;\n      currentTime += bounceTime;\n      keyframes.push({ time: currentTime, scale: undershoot });\n\n      // Overshoot again (smaller)\n      currentOvershoot = 1 + (currentOvershoot - 1) * bounceDecay * bounceDecay;\n      currentTime += bounceTime;\n      if (i < bounces - 1) {\n        keyframes.push({ time: currentTime, scale: currentOvershoot });\n      }\n    }\n\n    // Final settle\n    keyframes.push({ time: 1, scale: 1 });\n\n    return keyframes;\n  }\n\n  /**\n   * Easing function for smooth transitions\n   */\n  private easeOutQuad(t: number): number {\n    return 1 - (1 - t) * (1 - t);\n  }\n}\n","/**\n * IdleAnimationEngine.ts\n * Manages continuous ambient animations for idle images using the Web Animations API.\n *\n * Uses composite: 'add' for transform-based animations (wiggle, pulse, spin) so\n * idle animations layer on top of the base transform without replacing it.\n * Blink uses opacity with no composite.\n */\n\nimport type {\n  IdleAnimationConfig,\n  IdleWiggleConfig,\n  IdlePulseConfig,\n  IdleBlinkConfig,\n  IdleSpinConfig\n} from '../config/types';\nimport {\n  DEFAULT_IDLE_WIGGLE,\n  DEFAULT_IDLE_PULSE,\n  DEFAULT_IDLE_BLINK,\n  DEFAULT_IDLE_SPIN\n} from '../config/defaults';\n\ninterface IdleEntry {\n  element: HTMLElement;\n  index: number;\n  totalImages: number;\n  animation: Animation | null;\n  blinkAnimation: Animation | null;\n  customTeardown: (() => void) | null;\n  paused: boolean;\n  stopped: boolean;\n  startTimer: ReturnType<typeof setTimeout> | null;\n}\n\nexport class IdleAnimationEngine {\n  private config: IdleAnimationConfig;\n  private entries: Map<HTMLElement, IdleEntry> = new Map();\n  private entryDurationMs: number;\n\n  // Single rAF loop shared across all 'together'-sync animations.\n  // Each frame, all active together-mode animations get the same currentTime.\n  private togetherRafId: number | null = null;\n  private togetherSpeed: number = 0;\n\n  constructor(config: IdleAnimationConfig, entryDurationMs = 600) {\n    this.config = config;\n    this.entryDurationMs = entryDurationMs;\n  }\n\n  /**\n   * Register an image element for idle animation.\n   * Starts animation after entry duration completes.\n   */\n  register(element: HTMLElement, index: number, totalImages: number, entryDuration?: number): void {\n    if (this.entries.has(element)) return;\n\n    const delay = entryDuration ?? this.entryDurationMs;\n    const startDelay = this.config.startDelay ?? delay;\n\n    const entry: IdleEntry = {\n      element,\n      index,\n      totalImages,\n      animation: null,\n      blinkAnimation: null,\n      customTeardown: null,\n      paused: false,\n      stopped: false,\n      startTimer: null\n    };\n\n    this.entries.set(element, entry);\n\n    entry.startTimer = setTimeout(() => {\n      entry.startTimer = null;\n      if (!entry.stopped && !entry.paused) {\n        this._startAnimation(entry);\n      }\n    }, startDelay);\n  }\n\n  /**\n   * Pause idle animation for a specific image (set to neutral then pause).\n   */\n  pauseForImage(element: HTMLElement): void {\n    const entry = this.entries.get(element);\n    if (!entry) return;\n    entry.paused = true;\n    // Cancel start timer if still pending\n    if (entry.startTimer !== null) {\n      clearTimeout(entry.startTimer);\n      entry.startTimer = null;\n    }\n    this._pauseEntry(entry);\n  }\n\n  /**\n   * Resume idle animation for a specific image by starting a fresh animation.\n   * Always restarts rather than resuming, to avoid Web Animations API\n   * quirks with negative-delay animations after pause/cancel.\n   */\n  resumeForImage(element: HTMLElement): void {\n    const entry = this.entries.get(element);\n    if (!entry || entry.stopped) return;\n    entry.paused = false;\n    this._startAnimation(entry);\n  }\n\n  /**\n   * Stop and remove idle animation for a specific image.\n   */\n  stopForImage(element: HTMLElement): void {\n    const entry = this.entries.get(element);\n    if (!entry) return;\n    entry.stopped = true;\n    if (entry.startTimer !== null) {\n      clearTimeout(entry.startTimer);\n      entry.startTimer = null;\n    }\n    this._cancelEntry(entry);\n    this.entries.delete(element);\n  }\n\n  pauseAll(): void {\n    for (const entry of this.entries.values()) {\n      entry.paused = true;\n      if (entry.startTimer !== null) {\n        clearTimeout(entry.startTimer);\n        entry.startTimer = null;\n      }\n      this._pauseEntry(entry);\n    }\n  }\n\n  resumeAll(): void {\n    for (const entry of this.entries.values()) {\n      if (!entry.stopped) {\n        entry.paused = false;\n        this._startAnimation(entry);\n      }\n    }\n  }\n\n  stopAll(): void {\n    for (const entry of this.entries.values()) {\n      entry.stopped = true;\n      if (entry.startTimer !== null) {\n        clearTimeout(entry.startTimer);\n        entry.startTimer = null;\n      }\n      this._cancelEntry(entry);\n    }\n    this.entries.clear();\n    this._stopTogetherLoop();\n  }\n\n  // ──────────────────────────────────────────────────────────────────────────\n  // Private helpers\n  // ──────────────────────────────────────────────────────────────────────────\n\n  private _startAnimation(entry: IdleEntry): void {\n    const { type } = this.config;\n    switch (type) {\n      case 'wiggle':  this._startWiggle(entry); break;\n      case 'pulse':   this._startPulse(entry);  break;\n      case 'blink':   this._startBlink(entry);  break;\n      case 'spin':    this._startSpin(entry);   break;\n      case 'custom':  this._startCustom(entry); break;\n      default: break;\n    }\n  }\n\n  private _startWiggle(entry: IdleEntry): void {\n    const cfg: IdleWiggleConfig = { ...DEFAULT_IDLE_WIGGLE, ...this.config.wiggle };\n\n    const keyframes: Keyframe[] = [\n      { transform: 'rotate(0deg)',                  offset: 0    },\n      { transform: `rotate(${cfg.maxAngle}deg)`,    offset: 0.25 },\n      { transform: 'rotate(0deg)',                  offset: 0.5  },\n      { transform: `rotate(${-cfg.maxAngle}deg)`,   offset: 0.75 },\n      { transform: 'rotate(0deg)',                  offset: 1    }\n    ];\n\n    if (cfg.sync === 'together') {\n      entry.animation = entry.element.animate(keyframes, {\n        duration: cfg.speed, iterations: Infinity, composite: 'add', fill: 'both'\n      });\n      entry.animation.pause();\n      this._startTogetherLoop(cfg.speed);\n    } else {\n      entry.animation = entry.element.animate(keyframes, {\n        duration: cfg.speed,\n        delay: -(Math.random() * cfg.speed),\n        iterations: Infinity,\n        composite: 'add'\n      });\n    }\n  }\n\n  private _startPulse(entry: IdleEntry): void {\n    const cfg: IdlePulseConfig = { ...DEFAULT_IDLE_PULSE, ...this.config.pulse };\n\n    const keyframes: Keyframe[] = [\n      { transform: 'scale(1)',               offset: 0    },\n      { transform: `scale(${cfg.maxScale})`, offset: 0.25 },\n      { transform: 'scale(1)',               offset: 0.5  },\n      { transform: `scale(${cfg.minScale})`, offset: 0.75 },\n      { transform: 'scale(1)',               offset: 1    }\n    ];\n\n    if (cfg.sync === 'together') {\n      entry.animation = entry.element.animate(keyframes, {\n        duration: cfg.speed, iterations: Infinity, composite: 'add', fill: 'both'\n      });\n      entry.animation.pause();\n      this._startTogetherLoop(cfg.speed);\n    } else {\n      entry.animation = entry.element.animate(keyframes, {\n        duration: cfg.speed,\n        delay: -(Math.random() * cfg.speed),\n        iterations: Infinity,\n        composite: 'add'\n      });\n    }\n  }\n\n  private _startBlink(entry: IdleEntry): void {\n    const cfg: IdleBlinkConfig = { ...DEFAULT_IDLE_BLINK, ...this.config.blink };\n    const delay = -(Math.random() * cfg.speed);\n\n    // Use the element's current opacity as the \"visible\" state so blink\n    // respects the configured default (and hover) opacity.\n    const onOpacity = parseFloat(getComputedStyle(entry.element).opacity) || 1;\n\n    let keyframes: Keyframe[];\n    let options: KeyframeAnimationOptions;\n\n    if (cfg.style === 'fade') {\n      keyframes = [\n        { opacity: onOpacity, offset: 0   },\n        { opacity: 0,         offset: 0.5 },\n        { opacity: onOpacity, offset: 1   }\n      ];\n      options = {\n        duration: cfg.speed,\n        delay,\n        iterations: Infinity,\n        easing: 'ease-in-out'\n      };\n    } else {\n      // snap (default)\n      keyframes = [\n        { opacity: onOpacity, offset: 0              },\n        { opacity: onOpacity, offset: cfg.onRatio    },\n        { opacity: 0,         offset: Math.min(cfg.onRatio + 0.01, 0.99) },\n        { opacity: 0,         offset: 0.99           },\n        { opacity: onOpacity, offset: 1              }\n      ];\n      options = {\n        duration: cfg.speed,\n        delay,\n        iterations: Infinity\n      };\n    }\n\n    entry.blinkAnimation = entry.element.animate(keyframes, options);\n  }\n\n  private _startSpin(entry: IdleEntry): void {\n    const cfg: IdleSpinConfig = { ...DEFAULT_IDLE_SPIN, ...this.config.spin };\n    const endDeg = cfg.direction === 'clockwise' ? 360 : -360;\n\n    entry.animation = entry.element.animate(\n      [{ transform: 'rotate(0deg)' }, { transform: `rotate(${endDeg}deg)` }],\n      {\n        duration: cfg.speed,\n        iterations: Infinity,\n        easing: 'linear',\n        composite: 'add'\n      }\n    );\n  }\n\n  private _startCustom(entry: IdleEntry): void {\n    const fn = this.config.custom;\n    if (!fn) return;\n\n    const result = fn({ element: entry.element, index: entry.index, totalImages: entry.totalImages });\n\n    if (typeof result === 'function') {\n      entry.customTeardown = result;\n    } else if (result && typeof (result as Animation).play === 'function') {\n      entry.animation = result as Animation;\n    }\n  }\n\n  private _startTogetherLoop(speed: number): void {\n    this.togetherSpeed = speed;\n    if (this.togetherRafId !== null) return; // already ticking\n    const tick = () => {\n      const t = performance.now() % this.togetherSpeed;\n      for (const entry of this.entries.values()) {\n        if (!entry.stopped && !entry.paused && entry.animation) {\n          entry.animation.currentTime = t;\n        }\n      }\n      this.togetherRafId = requestAnimationFrame(tick);\n    };\n    this.togetherRafId = requestAnimationFrame(tick);\n  }\n\n  private _stopTogetherLoop(): void {\n    if (this.togetherRafId !== null) {\n      cancelAnimationFrame(this.togetherRafId);\n      this.togetherRafId = null;\n    }\n  }\n\n  private _pauseEntry(entry: IdleEntry): void {\n    // Cancel rather than pause — avoids Web Animations API quirks with\n    // negative-delay infinite animations. resumeForImage() always restarts fresh.\n    if (entry.animation) {\n      entry.animation.cancel();\n      entry.animation = null;\n    }\n    if (entry.blinkAnimation) {\n      entry.blinkAnimation.cancel();\n      entry.blinkAnimation = null;\n    }\n  }\n\n  private _cancelEntry(entry: IdleEntry): void {\n    if (entry.animation) {\n      entry.animation.cancel();\n      entry.animation = null;\n    }\n    if (entry.blinkAnimation) {\n      entry.blinkAnimation.cancel();\n      entry.blinkAnimation = null;\n    }\n    if (entry.customTeardown) {\n      entry.customTeardown();\n      entry.customTeardown = null;\n    }\n  }\n}\n","/**\n * RandomPlacementLayout.ts\n * Generates random overlapping layouts for image cloud\n */\n\nimport type { PlacementLayout, ImageLayout, ContainerBounds, LayoutConfig, ImageConfig } from '../config/types';\n\ninterface RandomLayoutOptions extends Partial<LayoutConfig> {\n  fixedHeight?: number;\n}\n\nexport class RandomPlacementLayout implements PlacementLayout {\n  private config: LayoutConfig;\n  private imageConfig: ImageConfig;\n\n  constructor(config: LayoutConfig, imageConfig: ImageConfig = {}) {\n    this.config = config;\n    this.imageConfig = imageConfig;\n  }\n\n  /**\n   * Generate random layout positions for images\n   * @param imageCount - Number of images to layout\n   * @param containerBounds - Container dimensions {width, height}\n   * @param options - Optional overrides (includes fixedHeight)\n   * @returns Array of layout objects with position, rotation, scale\n   */\n  generate(imageCount: number, containerBounds: ContainerBounds, options: RandomLayoutOptions = {}): ImageLayout[] {\n    const layouts: ImageLayout[] = [];\n    const { width, height } = containerBounds;\n\n    const padding = this.config.spacing.padding;\n    // Use fixedHeight if provided, otherwise use default 200\n    const baseImageSize = options.fixedHeight ?? 200;\n\n    // Get rotation config from image config\n    const rotationMode = this.imageConfig.rotation?.mode ?? 'none';\n    const minRotation = this.imageConfig.rotation?.range?.min ?? -15;\n    const maxRotation = this.imageConfig.rotation?.range?.max ?? 15;\n\n    // Get variance config from image config\n    const varianceMin = this.imageConfig.sizing?.variance?.min ?? 1.0;\n    const varianceMax = this.imageConfig.sizing?.variance?.max ?? 1.0;\n    const hasVariance = varianceMin !== 1.0 || varianceMax !== 1.0;\n\n    // Calculate safe bounds for center positions (accounting for half image size and padding)\n    // Use 16:9 aspect ratio (1.78) as maximum to handle most landscape images\n    const estAspectRatio = 1.5; // 3:2 - balanced for mixed portrait/landscape\n    const halfWidth = (baseImageSize * estAspectRatio) / 2;\n    const halfHeight = baseImageSize / 2;\n\n    const maxX = width - padding - halfWidth;\n    const maxY = height - padding - halfHeight;\n    const minX = padding + halfWidth;\n    const minY = padding + halfHeight;\n\n    for (let i = 0; i < imageCount; i++) {\n      // Random center position within safe bounds\n      const x = this.random(minX, maxX);\n      const y = this.random(minY, maxY);\n\n      // Random rotation within range (only when mode is random)\n      const rotation = rotationMode === 'random' ? this.random(minRotation, maxRotation) : 0;\n\n      // Random size variance\n      const scale = hasVariance ? this.random(varianceMin, varianceMax) : 1.0;\n      const scaledImageSize = baseImageSize * scale;\n\n      const layout: ImageLayout = {\n        id: i,\n        x,\n        y,\n        rotation,\n        scale,\n        baseSize: scaledImageSize\n      };\n\n      layouts.push(layout);\n    }\n\n    return layouts;\n  }\n\n  /**\n   * Utility: Generate random number between min and max\n   * @param min - Minimum value\n   * @param max - Maximum value\n   * @returns Random number in range\n   */\n  private random(min: number, max: number): number {\n    return Math.random() * (max - min) + min;\n  }\n}\n","/**\n * RadialPlacementLayout.ts\n * Generates concentric radial layouts for image cloud\n */\n\nimport type { PlacementLayout, ImageLayout, ContainerBounds, LayoutConfig, RadialAlgorithmConfig, ImageConfig } from '../config/types';\nimport { DEFAULT_RADIAL_CONFIG } from '../config/defaults';\n\ninterface RadialLayoutOptions extends Partial<LayoutConfig> {\n  fixedHeight?: number;\n}\n\nexport class RadialPlacementLayout implements PlacementLayout {\n  private config: LayoutConfig;\n  private imageConfig: ImageConfig;\n\n  constructor(config: LayoutConfig, imageConfig: ImageConfig = {}) {\n    this.config = config;\n    this.imageConfig = imageConfig;\n  }\n\n  /**\n   * Generate radial layout positions for images\n   * @param imageCount - Number of images to layout\n   * @param containerBounds - Container dimensions {width, height}\n   * @param options - Optional overrides\n   * @returns Array of layout objects with position, rotation, scale\n   */\n  generate(\n    imageCount: number,\n    containerBounds: ContainerBounds,\n    options: RadialLayoutOptions = {}\n  ): ImageLayout[] {\n    const layouts: ImageLayout[] = [];\n    const { width, height } = containerBounds;\n    // Use fixedHeight if provided, otherwise use default 200\n    const baseImageSize = options.fixedHeight ?? 200;\n\n    // Get rotation config from image config\n    const rotationMode = this.imageConfig.rotation?.mode ?? 'none';\n    const minRotation = this.imageConfig.rotation?.range?.min ?? -15;\n    const maxRotation = this.imageConfig.rotation?.range?.max ?? 15;\n\n    // Get variance config from image config\n    const varianceMin = this.imageConfig.sizing?.variance?.min ?? 1.0;\n    const varianceMax = this.imageConfig.sizing?.variance?.max ?? 1.0;\n    const hasVariance = varianceMin !== 1.0 || varianceMax !== 1.0;\n\n    // Get scale decay from layout config\n    const scaleDecay = this.config.scaleDecay ?? 0;\n\n    const radialConfig: RadialAlgorithmConfig = {\n      ...DEFAULT_RADIAL_CONFIG,\n      ...this.config.radial,\n    };\n\n    // Use override fixedHeight if provided, else baseImageSize\n    const imageSize = options.fixedHeight ?? baseImageSize;\n    const cx = width / 2;\n    const cy = height / 2;\n\n    // Calculate max rings for scale decay calculation\n    const estimatedMaxRings = Math.ceil(Math.sqrt(imageCount));\n\n    const padding = this.config.spacing.padding ?? 50;\n    const maxRadius = Math.max(imageSize * 0.8, Math.min(\n      cx - padding - imageSize / 2,\n      cy - padding - imageSize / 2\n    ));\n\n    // Add center image (using center position)\n    if (imageCount > 0) {\n      // Apply variance to center image\n      const varianceScale = hasVariance ? this.random(varianceMin, varianceMax) : 1.0;\n      const centerSize = imageSize * varianceScale;\n\n      layouts.push({\n        id: 0,\n        x: cx,\n        y: cy,\n        rotation: rotationMode === 'random' ? this.random(minRotation * 0.33, maxRotation * 0.33) : 0, // Less rotation for center\n        scale: varianceScale,\n        baseSize: centerSize,\n        zIndex: 100 // Center image is highest\n      });\n    }\n\n    let processedCount = 1;\n    let currentRing = 1;\n\n    while (processedCount < imageCount) {\n      // Calculate scale decay for this ring (center is largest, outer rings smaller)\n      const normalizedRing = currentRing / estimatedMaxRings;\n      const ringScale = scaleDecay > 0\n        ? 1 - (normalizedRing * scaleDecay * 0.5) // Max 50% size reduction\n        : 1.0;\n\n      // Ring settings\n      // Scale X more than Y to create horizontal oval shape\n      const ringStep = Math.max(imageSize * 0.8, (maxRadius / estimatedMaxRings) * 1.5 / radialConfig.tightness);\n      const radiusY = currentRing * ringStep;\n      const radiusX = radiusY * 1.5; // Horizontal stretching factor\n\n      const circumference = Math.PI * (3 * (radiusX + radiusY) - Math.sqrt((3 * radiusX + radiusY) * (radiusX + 3 * radiusY))); // Ramanujan's approximation\n\n      const estimatedItemWidth = this.estimateWidth(imageSize);\n      // Increase density by ~40% (1.1 -> 0.7)\n      const itemsInRing = Math.floor(circumference / (estimatedItemWidth * 0.7));\n\n      if (itemsInRing === 0) {\n        currentRing++;\n        continue;\n      }\n\n      const angleStep = (2 * Math.PI) / itemsInRing;\n\n      // Add offset of 20 degrees per ring\n      const ringOffset = currentRing * (20 * Math.PI / 180);\n\n      for (let i = 0; i < itemsInRing && processedCount < imageCount; i++) {\n        const angle = (i * angleStep) + ringOffset;\n\n        // Apply variance and scale decay\n        const varianceScale = hasVariance ? this.random(varianceMin, varianceMax) : 1.0;\n        const combinedScale = ringScale * varianceScale;\n        const scaledImageSize = imageSize * combinedScale;\n\n        // Calculate center position of image using elliptical formula (store center, not top-left)\n        let x = cx + Math.cos(angle) * radiusX;\n        let y = cy + Math.sin(angle) * radiusY;\n\n        // Boundary Clamping - clamp center position with conservative estimate\n        // Use 16:9 aspect ratio (1.78) as maximum to handle most landscape images\n        const estAspectRatio = 1.5; // 3:2 - balanced for mixed portrait/landscape\n        const halfWidth = (scaledImageSize * estAspectRatio) / 2;\n        const halfHeight = scaledImageSize / 2;\n\n        // Clamp X (center position)\n        if (x - halfWidth < padding) {\n          x = padding + halfWidth;\n        } else if (x + halfWidth > width - padding) {\n          x = width - padding - halfWidth;\n        }\n\n        // Clamp Y (center position)\n        if (y - halfHeight < padding) {\n          y = padding + halfHeight;\n        } else if (y + halfHeight > height - padding) {\n          y = height - padding - halfHeight;\n        }\n\n        const rotation = rotationMode === 'random' ? this.random(minRotation, maxRotation) : 0;\n\n        layouts.push({\n          id: processedCount,\n          x,\n          y,\n          rotation,\n          scale: combinedScale,\n          baseSize: scaledImageSize,\n          zIndex: Math.max(1, 100 - currentRing) // Outer rings have lower z-index\n        });\n\n        processedCount++;\n      }\n\n      currentRing++;\n    }\n\n    return layouts;\n  }\n\n  /**\n   * Estimate image width based on height\n   * Assumes landscape aspect ratio (approximately 1.4:1)\n   * @param height - Image height\n   * @returns Estimated width\n   */\n  private estimateWidth(height: number): number {\n    // Assume landscape aspect ratio approx 4:3 or 16:9 on average\n    // Using 1.4 ratio\n    return height * 1.4;\n  }\n\n  /**\n   * Utility: Generate random number between min and max\n   * @param min - Minimum value\n   * @param max - Maximum value\n   * @returns Random number in range\n   */\n  private random(min: number, max: number): number {\n    return Math.random() * (max - min) + min;\n  }\n}","/**\n * GridPlacementLayout.ts\n * Generates grid-based layouts with optional stagger and jitter\n */\n\nimport type { PlacementLayout, ImageLayout, ContainerBounds, LayoutConfig, GridAlgorithmConfig, ImageConfig } from '../config/types';\n\ninterface GridLayoutOptions extends Partial<LayoutConfig> {\n  fixedHeight?: number;\n}\n\nconst DEFAULT_GRID_CONFIG: GridAlgorithmConfig = {\n  columns: 'auto',\n  rows: 'auto',\n  stagger: 'none',\n  jitter: 0,\n  overlap: 0,\n  fillDirection: 'row',\n  alignment: 'center',\n  gap: 10,\n  overflowOffset: 0.25\n};\n\n// Offset pattern for stacking overflow images within cells\n// Each overflow layer cycles through these directions\n// Corners first, then cardinal directions\nconst OVERFLOW_OFFSET_PATTERN = [\n  { x: 1, y: 1 },    // bottom-right\n  { x: -1, y: -1 },  // upper-left\n  { x: 1, y: -1 },   // upper-right\n  { x: -1, y: 1 },   // bottom-left\n  { x: -1, y: 0 },   // left\n  { x: 1, y: 0 },    // right\n  { x: 0, y: -1 },   // up\n  { x: 0, y: 1 },    // down\n];\n\nexport class GridPlacementLayout implements PlacementLayout {\n  private config: LayoutConfig;\n  private imageConfig: ImageConfig;\n\n  constructor(config: LayoutConfig, imageConfig: ImageConfig = {}) {\n    this.config = config;\n    this.imageConfig = imageConfig;\n  }\n\n  /**\n   * Generate grid layout positions for images\n   * @param imageCount - Number of images to layout\n   * @param containerBounds - Container dimensions {width, height}\n   * @param options - Optional overrides (includes fixedHeight)\n   * @returns Array of layout objects with position, rotation, scale\n   */\n  generate(\n    imageCount: number,\n    containerBounds: ContainerBounds,\n    options: GridLayoutOptions = {}\n  ): ImageLayout[] {\n    const layouts: ImageLayout[] = [];\n    const { width, height } = containerBounds;\n\n    const gridConfig = { ...DEFAULT_GRID_CONFIG, ...this.config.grid };\n    const padding = this.config.spacing.padding;\n    // Use fixedHeight if provided, otherwise use base size from config\n    const baseImageSize = options.fixedHeight ?? 200;\n\n    // Get rotation config from image config\n    const rotationMode = this.imageConfig.rotation?.mode ?? 'none';\n\n    // Get variance config from image config\n    const varianceMin = this.imageConfig.sizing?.variance?.min ?? 1.0;\n    const varianceMax = this.imageConfig.sizing?.variance?.max ?? 1.0;\n    const hasVariance = varianceMin !== 1.0 || varianceMax !== 1.0;\n\n    // Calculate available space\n    const availableWidth = width - (2 * padding);\n    const availableHeight = height - (2 * padding);\n\n    // Calculate grid dimensions\n    const { columns, rows } = this.calculateGridDimensions(\n      imageCount,\n      availableWidth,\n      availableHeight,\n      baseImageSize,\n      gridConfig\n    );\n\n    // For stagger layouts, we need n+0.5 cells to fit in available space\n    // Calculate cell size accounting for this extra half-cell\n    const hasRowStagger = gridConfig.stagger === 'row';\n    const hasColumnStagger = gridConfig.stagger === 'column';\n\n    // Effective columns/rows for sizing: add 0.5 for stagger\n    const effectiveColumns = hasRowStagger ? columns + 0.5 : columns;\n    const effectiveRows = hasColumnStagger ? rows + 0.5 : rows;\n\n    // Calculate cell size to fit effective count in available space\n    const cellWidth = (availableWidth - (gridConfig.gap * (columns - 1))) / effectiveColumns;\n    const cellHeight = (availableHeight - (gridConfig.gap * (rows - 1))) / effectiveRows;\n\n    // Stagger offset is half a cell\n    const staggerOffsetX = hasRowStagger ? cellWidth / 2 : 0;\n    const staggerOffsetY = hasColumnStagger ? cellHeight / 2 : 0;\n\n    // Calculate image size with overlap factor\n    // overlap: 0 = fit in cell, 0.5 = 50% larger, 1.0 = 2x cell size\n    const overlapMultiplier = 1 + gridConfig.overlap;\n    const cellBasedSize = Math.min(cellWidth, cellHeight) * overlapMultiplier;\n\n    // Use fixedHeight if provided, otherwise use cell-based calculation\n    // For grid layouts, we respect fixedHeight but cap at cell-based size to avoid overflow\n    const imageSize = options.fixedHeight\n      ? Math.min(options.fixedHeight, cellBasedSize)\n      : cellBasedSize;\n\n    // Calculate total grid dimensions for alignment (include stagger offset)\n    const totalGridWidth = columns * cellWidth + (columns - 1) * gridConfig.gap + staggerOffsetX;\n    const totalGridHeight = rows * cellHeight + (rows - 1) * gridConfig.gap + staggerOffsetY;\n\n    // Center the grid in the container\n    const gridOffsetX = padding + (availableWidth - totalGridWidth) / 2;\n    const gridOffsetY = padding + (availableHeight - totalGridHeight) / 2;\n\n    // Detect overflow mode: when both dimensions are fixed and we have more images than cells\n    const cellCount = columns * rows;\n    const hasFixedGrid = gridConfig.columns !== 'auto' && gridConfig.rows !== 'auto';\n    const isOverflowMode = hasFixedGrid && imageCount > cellCount;\n\n    if (typeof window !== 'undefined') {\n      (window as any).__gridOverflowDebug = {\n        gridConfigColumns: gridConfig.columns,\n        gridConfigRows: gridConfig.rows,\n        columns, rows, cellCount,\n        hasFixedGrid, imageCount, isOverflowMode\n      };\n    }\n\n    // Track stack depth for each cell (used in overflow mode)\n    const cellStackCount: number[] = isOverflowMode ? new Array(cellCount).fill(0) : [];\n\n    // Calculate overflow offset in pixels\n    const overflowOffsetPx = Math.min(cellWidth, cellHeight) * gridConfig.overflowOffset;\n\n    for (let i = 0; i < imageCount; i++) {\n      let col: number;\n      let row: number;\n      let stackLayer = 0;  // 0 = base image, 1+ = overflow layers\n\n      if (isOverflowMode && i >= cellCount) {\n        // Overflow image: determine target cell and stack layer\n        const overflowIndex = i - cellCount;\n        const targetCell = overflowIndex % cellCount;\n        stackLayer = Math.floor(overflowIndex / cellCount) + 1;\n        cellStackCount[targetCell]++;\n\n        // Get cell coordinates from target cell index\n        if (gridConfig.fillDirection === 'row') {\n          col = targetCell % columns;\n          row = Math.floor(targetCell / columns);\n        } else {\n          row = targetCell % rows;\n          col = Math.floor(targetCell / rows);\n        }\n      } else {\n        // Base image: normal cell assignment\n        if (gridConfig.fillDirection === 'row') {\n          col = i % columns;\n          row = Math.floor(i / columns);\n        } else {\n          row = i % rows;\n          col = Math.floor(i / rows);\n        }\n      }\n\n      // Base cell position (center of cell)\n      let cellCenterX = gridOffsetX + col * (cellWidth + gridConfig.gap) + cellWidth / 2;\n      let cellCenterY = gridOffsetY + row * (cellHeight + gridConfig.gap) + cellHeight / 2;\n\n      // Apply stagger offset\n      if (gridConfig.stagger === 'row' && row % 2 === 1) {\n        cellCenterX += cellWidth / 2;\n      } else if (gridConfig.stagger === 'column' && col % 2 === 1) {\n        cellCenterY += cellHeight / 2;\n      }\n\n      // Apply overflow offset for stacked images\n      if (stackLayer > 0) {\n        const patternIndex = (stackLayer - 1) % OVERFLOW_OFFSET_PATTERN.length;\n        const pattern = OVERFLOW_OFFSET_PATTERN[patternIndex];\n        cellCenterX += pattern.x * overflowOffsetPx;\n        cellCenterY += pattern.y * overflowOffsetPx;\n      }\n\n      // Apply jitter (random offset within cell bounds)\n      if (gridConfig.jitter > 0) {\n        const maxJitterX = (cellWidth / 2) * gridConfig.jitter;\n        const maxJitterY = (cellHeight / 2) * gridConfig.jitter;\n        cellCenterX += this.random(-maxJitterX, maxJitterX);\n        cellCenterY += this.random(-maxJitterY, maxJitterY);\n      }\n\n      // Store center position directly (not top-left)\n      let x = cellCenterX;\n      let y = cellCenterY;\n\n      // Handle incomplete row alignment (only for non-overflow mode)\n      if (!isOverflowMode && gridConfig.fillDirection === 'row') {\n        const itemsInLastRow = imageCount % columns || columns;\n        const isLastRow = row === Math.floor((imageCount - 1) / columns);\n\n        if (isLastRow && itemsInLastRow < columns) {\n          const lastRowWidth = itemsInLastRow * cellWidth + (itemsInLastRow - 1) * gridConfig.gap;\n          let alignmentOffset = 0;\n\n          if (gridConfig.alignment === 'center') {\n            alignmentOffset = (totalGridWidth - lastRowWidth) / 2;\n          } else if (gridConfig.alignment === 'end') {\n            alignmentOffset = totalGridWidth - lastRowWidth;\n          }\n\n          x += alignmentOffset;\n        }\n      }\n\n      // Apply variance to create non-uniform look\n      const varianceScale = hasVariance ? this.random(varianceMin, varianceMax) : 1.0;\n      const scaledImageSize = imageSize * varianceScale;\n\n      // Boundary clamping for center-based positioning\n      // Use 1.5 multiplier (3:2 aspect) as reasonable middle ground for mixed portrait/landscape\n      const estAspectRatio = 1.5;\n      const halfWidth = (scaledImageSize * estAspectRatio) / 2;\n      const halfHeight = scaledImageSize / 2;\n      const minX = padding + halfWidth;\n      const maxX = width - padding - halfWidth;\n      const minY = padding + halfHeight;\n      const maxY = height - padding - halfHeight;\n\n      x = Math.max(minX, Math.min(x, maxX));\n      y = Math.max(minY, Math.min(y, maxY));\n\n      // Apply rotation when mode is random\n      // If jitter > 0, scale rotation by jitter factor; otherwise use full rotation range\n      let rotation = 0;\n      if (rotationMode === 'random') {\n        const minRotation = this.imageConfig.rotation?.range?.min ?? -15;\n        const maxRotation = this.imageConfig.rotation?.range?.max ?? 15;\n        if (gridConfig.jitter > 0) {\n          // Scale rotation by jitter factor for more subtle effect\n          rotation = this.random(minRotation * gridConfig.jitter, maxRotation * gridConfig.jitter);\n        } else {\n          // Full rotation range even without jitter\n          rotation = this.random(minRotation, maxRotation);\n        }\n      }\n\n      // Calculate z-index: in overflow mode, overflow images go BELOW base images\n      // Base images get higher z-index so they appear on top\n      // Overflow images get progressively lower z-index so they flow under\n      let zIndex: number;\n      if (isOverflowMode && stackLayer > 0) {\n        // Overflow images: lower z-index, decreasing with each layer\n        // Layer 1 = 50, Layer 2 = 49, etc.\n        zIndex = 50 - stackLayer;\n      } else {\n        // Base images: higher z-index (100+)\n        zIndex = isOverflowMode ? 100 + i : i + 1;\n      }\n\n      layouts.push({\n        id: i,\n        x,\n        y,\n        rotation,\n        scale: varianceScale,\n        baseSize: scaledImageSize,\n        zIndex\n      });\n    }\n\n    return layouts;\n  }\n\n  /**\n   * Calculate optimal grid dimensions based on image count and container\n   */\n  private calculateGridDimensions(\n    imageCount: number,\n    availableWidth: number,\n    availableHeight: number,\n    _baseImageSize: number,\n    config: GridAlgorithmConfig\n  ): { columns: number; rows: number } {\n    let columns: number;\n    let rows: number;\n\n    if (config.columns !== 'auto' && config.rows !== 'auto') {\n      columns = config.columns;\n      rows = config.rows;\n    } else if (config.columns !== 'auto') {\n      columns = config.columns;\n      rows = Math.ceil(imageCount / columns);\n    } else if (config.rows !== 'auto') {\n      rows = config.rows;\n      columns = Math.ceil(imageCount / rows);\n    } else {\n      // Auto-calculate: try to fill the space while maintaining reasonable aspect ratio\n      const aspectRatio = availableWidth / availableHeight;\n      const imageAspectRatio = 1.4; // Assumed landscape\n\n      // Calculate ideal columns based on aspect ratio\n      columns = Math.max(1, Math.round(Math.sqrt(imageCount * aspectRatio / imageAspectRatio)));\n      rows = Math.ceil(imageCount / columns);\n\n      // Ensure we don't have too many empty cells\n      while (columns > 1 && (columns - 1) * rows >= imageCount) {\n        columns--;\n      }\n    }\n\n    return { columns: Math.max(1, columns), rows: Math.max(1, rows) };\n  }\n\n  /**\n   * Utility: Generate random number between min and max\n   */\n  private random(min: number, max: number): number {\n    return Math.random() * (max - min) + min;\n  }\n}\n","/**\n * SpiralPlacementLayout.ts\n * Generates spiral layouts (golden, archimedean, logarithmic)\n */\n\nimport type { PlacementLayout, ImageLayout, ContainerBounds, LayoutConfig, SpiralAlgorithmConfig, ImageConfig } from '../config/types';\n\ninterface SpiralLayoutOptions extends Partial<LayoutConfig> {\n  fixedHeight?: number;\n}\n\n// Golden angle in radians (~137.5 degrees)\nconst GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));\n\nconst DEFAULT_SPIRAL_CONFIG: SpiralAlgorithmConfig = {\n  spiralType: 'golden',\n  direction: 'counterclockwise',\n  tightness: 1.0,\n  scaleDecay: 0,\n  startAngle: 0\n};\n\nexport class SpiralPlacementLayout implements PlacementLayout {\n  private config: LayoutConfig;\n  private imageConfig: ImageConfig;\n\n  constructor(config: LayoutConfig, imageConfig: ImageConfig = {}) {\n    this.config = config;\n    this.imageConfig = imageConfig;\n  }\n\n  /**\n   * Generate spiral layout positions for images\n   * @param imageCount - Number of images to layout\n   * @param containerBounds - Container dimensions {width, height}\n   * @param options - Optional overrides (includes fixedHeight)\n   * @returns Array of layout objects with position, rotation, scale\n   */\n  generate(\n    imageCount: number,\n    containerBounds: ContainerBounds,\n    options: SpiralLayoutOptions = {}\n  ): ImageLayout[] {\n    const layouts: ImageLayout[] = [];\n    const { width, height } = containerBounds;\n\n    const spiralConfig = { ...DEFAULT_SPIRAL_CONFIG, ...this.config.spiral };\n    const padding = this.config.spacing.padding;\n    // Use fixedHeight if provided, otherwise use base size from config\n    const baseImageSize = options.fixedHeight ?? 200;\n\n    // Get rotation config from image config\n    const rotationMode = this.imageConfig.rotation?.mode ?? 'none';\n    const minRotation = this.imageConfig.rotation?.range?.min ?? -15;\n    const maxRotation = this.imageConfig.rotation?.range?.max ?? 15;\n\n    // Get variance config from image config\n    const varianceMin = this.imageConfig.sizing?.variance?.min ?? 1.0;\n    const varianceMax = this.imageConfig.sizing?.variance?.max ?? 1.0;\n    const hasVariance = varianceMin !== 1.0 || varianceMax !== 1.0;\n\n    // Get scale decay from layout config (overrides spiral-specific config)\n    const scaleDecay = this.config.scaleDecay ?? spiralConfig.scaleDecay;\n\n    // Center of the spiral\n    const cx = width / 2;\n    const cy = height / 2;\n\n    // Calculate available radius (distance from center to edge)\n    const maxRadius = Math.min(\n      cx - padding - baseImageSize / 2,\n      cy - padding - baseImageSize / 2\n    );\n\n    // Direction multiplier (1 for counterclockwise, -1 for clockwise)\n    const directionMultiplier = spiralConfig.direction === 'clockwise' ? -1 : 1;\n\n    for (let i = 0; i < imageCount; i++) {\n      // Calculate angle based on spiral type\n      let angle: number;\n      let radius: number;\n\n      if (spiralConfig.spiralType === 'golden') {\n        // Golden spiral: each point separated by golden angle\n        // Creates optimal distribution like sunflower seeds\n        angle = i * GOLDEN_ANGLE * directionMultiplier + spiralConfig.startAngle;\n        // Radius grows with square root of index for even distribution\n        radius = this.calculateGoldenRadius(i, imageCount, maxRadius, spiralConfig.tightness);\n      } else if (spiralConfig.spiralType === 'archimedean') {\n        // Archimedean spiral: r = a + b*θ (constant spacing between arms)\n        const theta = i * 0.5 * spiralConfig.tightness;\n        angle = theta * directionMultiplier + spiralConfig.startAngle;\n        radius = this.calculateArchimedeanRadius(theta, imageCount, maxRadius, spiralConfig.tightness);\n      } else {\n        // Logarithmic spiral: r = a * e^(b*θ) (self-similar, appears in nature)\n        const theta = i * 0.3 * spiralConfig.tightness;\n        angle = theta * directionMultiplier + spiralConfig.startAngle;\n        radius = this.calculateLogarithmicRadius(theta, imageCount, maxRadius, spiralConfig.tightness);\n      }\n\n      // Convert polar to cartesian coordinates (store center position)\n      const x = cx + Math.cos(angle) * radius;\n      const y = cy + Math.sin(angle) * radius;\n\n      // Calculate scale based on decay (center images larger) - use scaleDecay from image config\n      const normalizedRadius = radius / maxRadius;\n      const decayScale = scaleDecay > 0\n        ? 1 - (normalizedRadius * scaleDecay * 0.5) // Max 50% size reduction\n        : 1.0;\n\n      // Apply variance\n      const varianceScale = hasVariance ? this.random(varianceMin, varianceMax) : 1.0;\n      const combinedScale = decayScale * varianceScale;\n\n      // Apply scaled image size\n      const scaledImageSize = baseImageSize * combinedScale;\n\n      // Clamp center positions to keep images within bounds\n      // Use 16:9 aspect ratio (1.78) as maximum to handle most landscape images\n      const estAspectRatio = 1.5; // 3:2 - balanced for mixed portrait/landscape\n      const halfWidth = (scaledImageSize * estAspectRatio) / 2;\n      const halfHeight = scaledImageSize / 2;\n      const minX = padding + halfWidth;\n      const maxX = width - padding - halfWidth;\n      const minY = padding + halfHeight;\n      const maxY = height - padding - halfHeight;\n\n      const clampedX = Math.max(minX, Math.min(x, maxX));\n      const clampedY = Math.max(minY, Math.min(y, maxY));\n\n      // Rotation based on mode\n      let rotation = 0;\n      if (rotationMode === 'random') {\n        const baseRotation = (angle * 180 / Math.PI) % 360;\n        const rotationVariance = this.random(minRotation, maxRotation);\n        rotation = spiralConfig.spiralType === 'golden'\n          ? rotationVariance // Pure random for golden (more organic)\n          : (baseRotation * 0.1 + rotationVariance * 0.9); // Slight directional bias for others\n      } else if (rotationMode === 'tangent') {\n        // Tangent rotation: align image to spiral curve\n        rotation = this.calculateSpiralTangent(angle, radius, spiralConfig);\n      }\n\n      // Z-index: center images on top\n      const zIndex = imageCount - i;\n\n      layouts.push({\n        id: i,\n        x: clampedX,\n        y: clampedY,\n        rotation,\n        scale: combinedScale,\n        baseSize: scaledImageSize,\n        zIndex\n      });\n    }\n\n    return layouts;\n  }\n\n  /**\n   * Calculate tangent angle for spiral curve at given position\n   * This aligns the image along the spiral's direction of travel\n   */\n  private calculateSpiralTangent(\n    angle: number,\n    radius: number,\n    spiralConfig: SpiralAlgorithmConfig\n  ): number {\n    // For different spiral types, the tangent calculation varies\n    // The tangent angle is the derivative of the spiral equation\n\n    let tangentAngle: number;\n\n    if (spiralConfig.spiralType === 'golden') {\n      // Golden spiral tangent is approximately perpendicular to radial line\n      // Plus a small offset based on the golden angle\n      tangentAngle = angle + Math.PI / 2;\n    } else if (spiralConfig.spiralType === 'archimedean') {\n      // Archimedean spiral: dr/dθ = constant = b\n      // tan(ψ) = r / (dr/dθ) = r / b\n      // For tightness = 1, b ≈ 1\n      const b = 1 / spiralConfig.tightness;\n      const psi = Math.atan(radius / b);\n      tangentAngle = angle + psi;\n    } else {\n      // Logarithmic spiral: tangent makes constant angle with radial line\n      // This angle depends on the growth rate b\n      const b = 0.15 / spiralConfig.tightness;\n      const psi = Math.atan(1 / b); // Constant pitch angle\n      tangentAngle = angle + psi;\n    }\n\n    // Convert to degrees and normalize\n    const degrees = (tangentAngle * 180 / Math.PI) % 360;\n\n    // Adjust so 0 degrees means pointing right\n    return degrees - 90;\n  }\n\n  /**\n   * Calculate radius for golden spiral (Vogel's model)\n   * Creates even distribution like sunflower seeds\n   */\n  private calculateGoldenRadius(\n    index: number,\n    totalImages: number,\n    maxRadius: number,\n    tightness: number\n  ): number {\n    // Vogel's model: r = c * sqrt(n)\n    // Scaling factor to fit within maxRadius\n    const scalingFactor = maxRadius / Math.sqrt(totalImages);\n    const radius = scalingFactor * Math.sqrt(index) / tightness;\n    return Math.min(radius, maxRadius);\n  }\n\n  /**\n   * Calculate radius for Archimedean spiral\n   * r = a + b*θ (constant spacing between arms)\n   */\n  private calculateArchimedeanRadius(\n    theta: number,\n    totalImages: number,\n    maxRadius: number,\n    tightness: number\n  ): number {\n    // Scale so that the last point is at maxRadius\n    const maxTheta = totalImages * 0.5 * tightness;\n    const normalizedTheta = theta / maxTheta;\n    return normalizedTheta * maxRadius;\n  }\n\n  /**\n   * Calculate radius for logarithmic (equiangular) spiral\n   * r = a * e^(b*θ)\n   */\n  private calculateLogarithmicRadius(\n    theta: number,\n    totalImages: number,\n    maxRadius: number,\n    tightness: number\n  ): number {\n    // Parameters for logarithmic spiral\n    const a = maxRadius * 0.05; // Starting radius\n    const b = 0.15 / tightness; // Growth rate\n\n    const radius = a * Math.exp(b * theta);\n\n    // Normalize to fit within maxRadius\n    const maxTheta = totalImages * 0.3 * tightness;\n    const maxComputedRadius = a * Math.exp(b * maxTheta);\n\n    return (radius / maxComputedRadius) * maxRadius;\n  }\n\n  /**\n   * Utility: Generate random number between min and max\n   */\n  private random(min: number, max: number): number {\n    return Math.random() * (max - min) + min;\n  }\n}\n","/**\n * ClusterPlacementLayout.ts\n * Generates organic cluster layouts with natural groupings\n */\n\nimport type { PlacementLayout, ImageLayout, ContainerBounds, LayoutConfig, ClusterAlgorithmConfig, ImageConfig } from '../config/types';\n\ninterface ClusterCenter {\n  x: number;\n  y: number;\n  spread: number; // Actual spread for this cluster (may vary if density='varied')\n}\n\ninterface ClusterLayoutOptions extends Partial<LayoutConfig> {\n  fixedHeight?: number;\n}\n\nconst DEFAULT_CLUSTER_CONFIG: ClusterAlgorithmConfig = {\n  clusterCount: 'auto',\n  clusterSpread: 150,\n  clusterSpacing: 200,\n  density: 'uniform',\n  overlap: 0.3,\n  distribution: 'gaussian'\n};\n\nexport class ClusterPlacementLayout implements PlacementLayout {\n  private config: LayoutConfig;\n  private imageConfig: ImageConfig;\n\n  constructor(config: LayoutConfig, imageConfig: ImageConfig = {}) {\n    this.config = config;\n    this.imageConfig = imageConfig;\n  }\n\n  /**\n   * Generate cluster layout positions for images\n   * @param imageCount - Number of images to layout\n   * @param containerBounds - Container dimensions {width, height}\n   * @param options - Optional overrides (includes fixedHeight)\n   * @returns Array of layout objects with position, rotation, scale\n   */\n  generate(\n    imageCount: number,\n    containerBounds: ContainerBounds,\n    options: ClusterLayoutOptions = {}\n  ): ImageLayout[] {\n    const layouts: ImageLayout[] = [];\n    const { width, height } = containerBounds;\n\n    const clusterConfig = { ...DEFAULT_CLUSTER_CONFIG, ...this.config.cluster };\n    const padding = this.config.spacing.padding;\n    // Use fixedHeight if provided, otherwise use default 200\n    const baseImageSize = options.fixedHeight ?? 200;\n\n    // Get rotation config from image config\n    const rotationMode = this.imageConfig.rotation?.mode ?? 'none';\n    const minRotation = this.imageConfig.rotation?.range?.min ?? -15;\n    const maxRotation = this.imageConfig.rotation?.range?.max ?? 15;\n\n    // Get variance config from image config\n    const varianceMin = this.imageConfig.sizing?.variance?.min ?? 1.0;\n    const varianceMax = this.imageConfig.sizing?.variance?.max ?? 1.0;\n    const hasVariance = varianceMin !== 1.0 || varianceMax !== 1.0;\n\n    // Calculate number of clusters\n    const clusterCount = this.calculateClusterCount(\n      imageCount,\n      clusterConfig.clusterCount,\n      width,\n      height,\n      clusterConfig.clusterSpacing\n    );\n\n    // Generate cluster centers with spacing constraints\n    const clusterCenters = this.generateClusterCenters(\n      clusterCount,\n      width,\n      height,\n      padding,\n      clusterConfig\n    );\n\n    // Assign images to clusters (round-robin for even distribution)\n    const imagesPerCluster = new Array(clusterCount).fill(0);\n    for (let i = 0; i < imageCount; i++) {\n      imagesPerCluster[i % clusterCount]++;\n    }\n\n    let imageIndex = 0;\n\n    // Place images in each cluster\n    for (let clusterIdx = 0; clusterIdx < clusterCount; clusterIdx++) {\n      const cluster = clusterCenters[clusterIdx];\n      const imagesInThisCluster = imagesPerCluster[clusterIdx];\n\n      for (let i = 0; i < imagesInThisCluster; i++) {\n        // Calculate position within cluster\n        let offsetX: number;\n        let offsetY: number;\n\n        if (clusterConfig.distribution === 'gaussian') {\n          // Gaussian distribution - most images near center, fewer at edges\n          offsetX = this.gaussianRandom() * cluster.spread;\n          offsetY = this.gaussianRandom() * cluster.spread;\n        } else {\n          // Uniform distribution within circle\n          const angle = this.random(0, Math.PI * 2);\n          const distance = this.random(0, cluster.spread);\n          offsetX = Math.cos(angle) * distance;\n          offsetY = Math.sin(angle) * distance;\n        }\n\n        // Apply overlap factor - pulls images closer to center and increases size\n        const overlapMultiplier = 1 + clusterConfig.overlap * 0.5;\n        const sizeMultiplier = 1 + clusterConfig.overlap * 0.3;\n\n        // Reduce offset based on overlap (images cluster tighter)\n        offsetX /= overlapMultiplier;\n        offsetY /= overlapMultiplier;\n\n        // Apply variance\n        const varianceScale = hasVariance ? this.random(varianceMin, varianceMax) : 1.0;\n        const combinedScale = sizeMultiplier * varianceScale;\n\n        // Calculate image size with overlap factor and variance\n        const imageSize = baseImageSize * combinedScale;\n\n        // Store center position (not top-left)\n        let x = cluster.x + offsetX;\n        let y = cluster.y + offsetY;\n\n        // Boundary clamping with center position\n        // Use 16:9 aspect ratio (1.78) as maximum to handle most landscape images\n        const estAspectRatio = 1.5; // 3:2 - balanced for mixed portrait/landscape\n        const halfWidth = (imageSize * estAspectRatio) / 2;\n        const halfHeight = imageSize / 2;\n        x = Math.max(padding + halfWidth, Math.min(x, width - padding - halfWidth));\n        y = Math.max(padding + halfHeight, Math.min(y, height - padding - halfHeight));\n\n        // Rotation - more variance for organic feel (only when mode is random)\n        const rotation = rotationMode === 'random' ? this.random(minRotation, maxRotation) : 0;\n\n        // Z-index: images closer to cluster center are on top\n        const distanceFromCenter = Math.sqrt(offsetX * offsetX + offsetY * offsetY);\n        const normalizedDistance = distanceFromCenter / cluster.spread;\n        const zIndex = Math.round((1 - normalizedDistance) * 50) + 1;\n\n        layouts.push({\n          id: imageIndex,\n          x,\n          y,\n          rotation,\n          scale: combinedScale,\n          baseSize: imageSize,\n          zIndex\n        });\n\n        imageIndex++;\n      }\n    }\n\n    return layouts;\n  }\n\n  /**\n   * Calculate optimal number of clusters based on image count and container\n   */\n  private calculateClusterCount(\n    imageCount: number,\n    configCount: number | 'auto',\n    width: number,\n    height: number,\n    clusterSpacing: number\n  ): number {\n    if (configCount !== 'auto') {\n      return Math.max(1, Math.min(configCount, imageCount));\n    }\n\n    // Auto-calculate based on container size and image count\n    // Aim for 5-15 images per cluster\n    const idealImagesPerCluster = 8;\n    const countByImages = Math.max(1, Math.ceil(imageCount / idealImagesPerCluster));\n\n    // Also consider container size - how many clusters can fit\n    const countBySpace = Math.floor(\n      (width / clusterSpacing) * (height / clusterSpacing) * 0.6\n    );\n\n    return Math.max(1, Math.min(countByImages, countBySpace, 10));\n  }\n\n  /**\n   * Generate cluster center positions with spacing constraints\n   */\n  private generateClusterCenters(\n    count: number,\n    width: number,\n    height: number,\n    padding: number,\n    config: ClusterAlgorithmConfig\n  ): ClusterCenter[] {\n    const centers: ClusterCenter[] = [];\n    const maxAttempts = 100;\n\n    // Available area for cluster centers\n    const minX = padding + config.clusterSpread;\n    const maxX = width - padding - config.clusterSpread;\n    const minY = padding + config.clusterSpread;\n    const maxY = height - padding - config.clusterSpread;\n\n    for (let i = 0; i < count; i++) {\n      let bestCandidate: ClusterCenter | null = null;\n      let bestMinDistance = -1;\n\n      // Try multiple random positions and pick the one with best spacing\n      for (let attempt = 0; attempt < maxAttempts; attempt++) {\n        const candidate = {\n          x: this.random(minX, maxX),\n          y: this.random(minY, maxY),\n          spread: this.calculateClusterSpread(config)\n        };\n\n        // Calculate minimum distance to existing clusters\n        let minDistance = Infinity;\n        for (const existing of centers) {\n          const dx = candidate.x - existing.x;\n          const dy = candidate.y - existing.y;\n          const distance = Math.sqrt(dx * dx + dy * dy);\n          minDistance = Math.min(minDistance, distance);\n        }\n\n        // First cluster or better spacing\n        if (centers.length === 0 || minDistance > bestMinDistance) {\n          bestCandidate = candidate;\n          bestMinDistance = minDistance;\n        }\n\n        // Good enough spacing found\n        if (minDistance >= config.clusterSpacing) {\n          break;\n        }\n      }\n\n      if (bestCandidate) {\n        centers.push(bestCandidate);\n      }\n    }\n\n    return centers;\n  }\n\n  /**\n   * Calculate spread for a cluster (may vary if density='varied')\n   */\n  private calculateClusterSpread(config: ClusterAlgorithmConfig): number {\n    if (config.density === 'uniform') {\n      return config.clusterSpread;\n    }\n\n    // Varied density: spread varies between 50% and 150% of config value\n    return config.clusterSpread * this.random(0.5, 1.5);\n  }\n\n  /**\n   * Generate a random number with approximately Gaussian distribution\n   * Using Box-Muller transform\n   */\n  private gaussianRandom(): number {\n    let u = 0, v = 0;\n    while (u === 0) u = Math.random();\n    while (v === 0) v = Math.random();\n\n    const value = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);\n\n    // Clamp to reasonable range (-3 to 3 std deviations)\n    return Math.max(-3, Math.min(3, value)) / 3;\n  }\n\n  /**\n   * Utility: Generate random number between min and max\n   */\n  private random(min: number, max: number): number {\n    return Math.random() * (max - min) + min;\n  }\n}\n","/**\n * WavePlacementLayout.ts\n * Generates wave-based layouts for image cloud\n */\n\nimport type { PlacementLayout, ImageLayout, ContainerBounds, LayoutConfig, ImageConfig } from '../config/types';\nimport { DEFAULT_WAVE_CONFIG } from '../config/defaults';\n\ninterface WaveLayoutOptions extends Partial<LayoutConfig> {\n  fixedHeight?: number;\n}\n\nexport class WavePlacementLayout implements PlacementLayout {\n  private config: LayoutConfig;\n  private imageConfig: ImageConfig;\n\n  constructor(config: LayoutConfig, imageConfig: ImageConfig = {}) {\n    this.config = config;\n    this.imageConfig = imageConfig;\n  }\n\n  /**\n   * Generate wave layout positions for images\n   * @param imageCount - Number of images to layout\n   * @param containerBounds - Container dimensions {width, height}\n   * @param options - Optional overrides\n   * @returns Array of layout objects with position, rotation, scale\n   */\n  generate(\n    imageCount: number,\n    containerBounds: ContainerBounds,\n    options: WaveLayoutOptions = {}\n  ): ImageLayout[] {\n    const layouts: ImageLayout[] = [];\n    const { width, height } = containerBounds;\n    // Use fixedHeight if provided, otherwise use default 200\n    const baseImageSize = options.fixedHeight ?? 200;\n    const padding = this.config.spacing.padding ?? 50;\n\n    // Get rotation config from image config\n    const rotationMode = this.imageConfig.rotation?.mode ?? 'none';\n    const minRotation = this.imageConfig.rotation?.range?.min ?? -15;\n    const maxRotation = this.imageConfig.rotation?.range?.max ?? 15;\n\n    // Get variance config from image config\n    const varianceMin = this.imageConfig.sizing?.variance?.min ?? 1.0;\n    const varianceMax = this.imageConfig.sizing?.variance?.max ?? 1.0;\n    const hasVariance = varianceMin !== 1.0 || varianceMax !== 1.0;\n\n    // Use override fixedHeight if provided, else baseImageSize\n    const imageSize = options.fixedHeight ?? baseImageSize;\n\n    // Get wave configuration, merging user config with defaults\n    const waveConfig = {\n      ...DEFAULT_WAVE_CONFIG,\n      ...this.config.wave\n    };\n\n    const { rows, amplitude, frequency, phaseShift, synchronization } = waveConfig;\n\n    // Calculate images per row (distribute evenly)\n    const imagesPerRow = Math.ceil(imageCount / rows);\n\n    // Estimate image width based on height and typical aspect ratio\n    const estAspectRatio = 1.5; // 3:2 - balanced for mixed portrait/landscape\n    const estImageWidth = imageSize * estAspectRatio;\n    const halfImageWidth = estImageWidth / 2;\n\n    // Calculate available horizontal space (accounting for image width at edges)\n    // This ensures images don't get compressed against the edges\n    const startX = padding + halfImageWidth;\n    const endX = width - padding - halfImageWidth;\n    const availableWidth = endX - startX;\n\n    // Distribute images evenly between startX and endX\n    const horizontalSpacing = imagesPerRow > 1 ? availableWidth / (imagesPerRow - 1) : 0;\n\n    // Calculate vertical distribution to fill the screen\n    // Row centerlines need room for amplitude swing above and below\n    const minCenterY = padding + amplitude + (imageSize / 2);\n    const maxCenterY = height - padding - amplitude - (imageSize / 2);\n    const availableHeight = maxCenterY - minCenterY;\n\n    // Calculate spacing between row centerlines\n    const rowSpacing = rows > 1 ? availableHeight / (rows - 1) : 0;\n\n    let imageIndex = 0;\n\n    for (let rowIndex = 0; rowIndex < rows && imageIndex < imageCount; rowIndex++) {\n      // Calculate base Y position (centerline) for this row - evenly distributed\n      const baseY = rows === 1\n        ? (minCenterY + maxCenterY) / 2  // Single row: center vertically\n        : minCenterY + (rowIndex * rowSpacing);\n\n      // Calculate phase offset based on synchronization mode\n      let phase = 0;\n      if (synchronization === 'offset') {\n        phase = rowIndex * phaseShift;\n      } else if (synchronization === 'alternating') {\n        phase = rowIndex * Math.PI;\n      }\n      // If 'synchronized', phase remains 0\n\n      // Place images along this wave row\n      for (let imgInRow = 0; imgInRow < imagesPerRow && imageIndex < imageCount; imgInRow++) {\n        // Calculate center position - evenly distributed within available space\n        // For single image, center it; otherwise distribute from startX to endX\n        const centerX = imagesPerRow === 1\n          ? (startX + endX) / 2\n          : startX + (imgInRow * horizontalSpacing);\n\n        // Calculate wave displacement based on center position\n        const waveY = this.calculateWaveY(centerX, width, amplitude, frequency, phase);\n\n        // Store center position directly (CSS transform will handle centering)\n        const x = centerX;\n        const y = baseY + waveY;\n\n        // Apply variance\n        const varianceScale = hasVariance ? this.random(varianceMin, varianceMax) : 1.0;\n        const scaledImageSize = imageSize * varianceScale;\n\n        // Calculate rotation based on image.rotation.mode\n        let rotation = 0;\n        if (rotationMode === 'tangent') {\n          // Follow wave tangent - images rotate to align with wave curve\n          rotation = this.calculateRotation(centerX, width, amplitude, frequency, phase);\n        } else if (rotationMode === 'random') {\n          // Random rotation within configured range\n          rotation = this.random(minRotation, maxRotation);\n        }\n        // If mode is 'none', rotation stays 0\n\n        // Clamp center positions to keep images within bounds\n        // Use 16:9 aspect ratio (1.78) as maximum to handle most landscape images\n        const estAspectRatio = 1.5; // 3:2 - balanced for mixed portrait/landscape\n        const halfWidth = (scaledImageSize * estAspectRatio) / 2;\n        const halfHeight = scaledImageSize / 2;\n        const minX = padding + halfWidth;\n        const maxX = width - padding - halfWidth;\n        const minY = padding + halfHeight;\n        const maxY = height - padding - halfHeight;\n\n        layouts.push({\n          id: imageIndex,\n          x: Math.max(minX, Math.min(x, maxX)),\n          y: Math.max(minY, Math.min(y, maxY)),\n          rotation,\n          scale: varianceScale,\n          baseSize: scaledImageSize,\n          zIndex: imageIndex + 1\n        });\n\n        imageIndex++;\n      }\n    }\n\n    return layouts;\n  }\n\n  /**\n   * Calculate Y position displacement on wave curve\n   * @param x - Horizontal position\n   * @param containerWidth - Container width\n   * @param amplitude - Wave amplitude\n   * @param frequency - Wave frequency\n   * @param phase - Phase offset\n   * @returns Y displacement from baseline\n   */\n  private calculateWaveY(\n    x: number,\n    containerWidth: number,\n    amplitude: number,\n    frequency: number,\n    phase: number\n  ): number {\n    // Normalize x to [0, 1] range\n    const normalizedX = x / containerWidth;\n\n    // Calculate wave: y = amplitude * sin(frequency * x * 2π + phase)\n    return amplitude * Math.sin(frequency * normalizedX * 2 * Math.PI + phase);\n  }\n\n  /**\n   * Calculate rotation based on wave tangent\n   * @param x - Horizontal position\n   * @param containerWidth - Container width\n   * @param amplitude - Wave amplitude\n   * @param frequency - Wave frequency\n   * @param phase - Phase offset\n   * @returns Rotation angle in degrees\n   */\n  private calculateRotation(\n    x: number,\n    containerWidth: number,\n    amplitude: number,\n    frequency: number,\n    phase: number\n  ): number {\n    // Normalize x to [0, 1] range\n    const normalizedX = x / containerWidth;\n\n    // Derivative of sine wave: amplitude * frequency * cos(frequency * x * 2π + phase)\n    const derivative = amplitude * frequency * 2 * Math.PI * Math.cos(frequency * normalizedX * 2 * Math.PI + phase) / containerWidth;\n\n    // Convert derivative to rotation angle in degrees\n    return Math.atan(derivative) * (180 / Math.PI);\n  }\n\n  /**\n   * Estimate image width based on height\n  /**\n   * Utility: Generate random number between min and max\n   * @param min - Minimum value\n   * @param max - Maximum value\n   * @returns Random number in range\n   */\n  private random(min: number, max: number): number {\n    return Math.random() * (max - min) + min;\n  }\n}\n","/**\n * hexagonGeometry.ts\n * Single source of truth for hexagonal grid math.\n * Used by clipPathGenerator.ts and HoneycombPlacementLayout.ts.\n */\n\n/** Reference height for the canonical hexagon definition (100px) */\nexport const HEXAGON_REF_HEIGHT = 100;\n\n/**\n * Regular flat-top hexagon reference points (refHeight = 100).\n * Circumradius r = 100/√3 so that height = r√3 = 100 exactly.\n * Width = 2r = 200/√3 ≈ 115.47.  All 6 vertices are equidistant from center.\n *\n *   x = r/2  = 50/√3 ≈ 28.87    (upper/lower inner)\n *   x = 3r/2 = 150/√3 ≈ 86.60   (upper/lower outer)\n *   x = 2r   = 200/√3 ≈ 115.47  (mid right vertex)\n */\nconst _R = 100 / Math.sqrt(3);\nexport const HEXAGON_REF_POINTS: Array<[number, number]> = [\n  [_R / 2,     0  ],   // upper-left\n  [3 * _R / 2, 0  ],   // upper-right\n  [2 * _R,     50 ],   // right  ← tiling key vertex\n  [3 * _R / 2, 100],   // lower-right\n  [_R / 2,     100],   // lower-left\n  [0,          50 ],   // left\n];\n\n/**\n * Col-step ratio = x of upper-right vertex / refHeight = (3r/2) / 100 = √3/2 ≈ 0.866.\n * (Previously 0.75 with the non-regular hexagon.)\n */\nexport const HEXAGON_COL_STEP_RATIO = HEXAGON_REF_POINTS[1][0] / HEXAGON_REF_HEIGHT; // √3/2 ≈ 0.866\n\n/** Row-offset ratio = y of right vertex / refHeight = 50/100 = 0.50 (unchanged) */\nexport const HEXAGON_ROW_OFFSET_RATIO = HEXAGON_REF_POINTS[2][1] / HEXAGON_REF_HEIGHT; // 0.50\n\n/**\n * Returns tiling parameters for a hexagon of the given height.\n * Derived from the canonical reference points, not hardcoded constants.\n */\nexport function getHexTilingParams(hexH: number): { colStep: number; rowOffset: number } {\n  return {\n    colStep: HEXAGON_COL_STEP_RATIO * hexH,\n    rowOffset: HEXAGON_ROW_OFFSET_RATIO * hexH,\n  };\n}\n\n/**\n * Converts cube coordinates (cx, cy, cz) to pixel center position.\n * @param cx - Cube coord x (cx + cy + cz = 0)\n * @param cy - Cube coord y\n * @param cz - Cube coord z\n * @param originX - Pixel origin X (container center)\n * @param originY - Pixel origin Y (container center)\n * @param hexH - Visual hex height (= imageHeight for height-relative clip path)\n */\nexport function hexCubeToPixel(\n  cx: number, cy: number, _cz: number,\n  originX: number, originY: number,\n  hexH: number\n): { px: number; py: number } {\n  const { colStep } = getHexTilingParams(hexH);\n  return {\n    px: originX + colStep * cx,\n    py: originY + hexH * (cy + cx / 2),\n  };\n}\n\n/** 6 cube direction vectors for clockwise ring traversal starting from top */\nexport const HEX_RING_DIRECTIONS: Array<[number, number, number]> = [\n  [+1,  0, -1],\n  [ 0, +1, -1],\n  [-1, +1,  0],\n  [-1,  0, +1],\n  [ 0, -1, +1],\n  [+1, -1,  0],\n];\n\n/**\n * Returns ordered cube coordinates for all cells in ring k, clockwise from the top.\n * Ring 0: [(0,0,0)]. Ring k: 6k cells.\n */\nexport function getHexRingCells(ring: number): Array<[number, number, number]> {\n  if (ring === 0) return [[0, 0, 0]];\n  const cells: Array<[number, number, number]> = [];\n  let [cx, cy, cz] = [0, -ring, ring]; // start at top\n  for (const [dx, dy, dz] of HEX_RING_DIRECTIONS) {\n    for (let step = 0; step < ring; step++) {\n      cells.push([cx, cy, cz]);\n      cx += dx; cy += dy; cz += dz;\n    }\n  }\n  return cells;\n}\n","/**\n * HoneycombPlacementLayout.ts\n * Places images in hexagonal rings, filling outward clockwise from center-top.\n * Default/hover clip paths are forced to hexagon height-relative by mergeConfig().\n */\n\nimport type { PlacementLayout, ImageLayout, ContainerBounds, LayoutConfig, ImageConfig } from '../config/types';\nimport { DEFAULT_HONEYCOMB_CONFIG } from '../config/defaults';\nimport { getHexRingCells, hexCubeToPixel } from '../utils/hexagonGeometry';\n\nexport class HoneycombPlacementLayout implements PlacementLayout {\n  private config: LayoutConfig;\n\n  // imageConfig intentionally not stored — honeycomb forces uniform sizing (rotation/variance\n  // would break hex tiling). Kept as parameter for interface compatibility.\n  constructor(config: LayoutConfig, _imageConfig: ImageConfig = {}) {\n    this.config = config;\n  }\n\n  generate(\n    imageCount: number,\n    containerBounds: ContainerBounds,\n    options: Partial<LayoutConfig> & { fixedHeight?: number } = {}\n  ): ImageLayout[] {\n    const layouts: ImageLayout[] = [];\n    const { width, height } = containerBounds;\n    const containerCX = width / 2;\n    const containerCY = height / 2;\n\n    const baseImageSize = options.fixedHeight ?? 200;\n\n    // Merge honeycomb config with defaults\n    const honeycombConfig = {\n      ...DEFAULT_HONEYCOMB_CONFIG,\n      ...this.config.honeycomb\n    };\n\n    const spacing = honeycombConfig.spacing ?? 0;\n\n    // hexH is the pitch (hex height + gap). Since the clip path is height-relative,\n    // the hex's visual height equals baseImageSize. We add spacing on top.\n    // Note: baseImageSize is already capped to fit the container by LayoutEngine.calculateAdaptiveSize().\n    const hexH = baseImageSize + spacing;\n\n    let placed = 0;\n    let ring = 0;\n\n    while (placed < imageCount) {\n      const cells = getHexRingCells(ring);\n\n      for (const [cx, cy, cz] of cells) {\n        if (placed >= imageCount) break;\n\n        const { px, py } = hexCubeToPixel(cx, cy, cz, containerCX, containerCY, hexH);\n\n        layouts.push({\n          id: placed,\n          x: px,\n          y: py,\n          rotation: 0,\n          scale: 1.0,\n          baseSize: baseImageSize,\n          // Inner rings render above outer rings\n          zIndex: Math.max(1, 100 - ring)\n        });\n\n        placed++;\n      }\n\n      ring++;\n    }\n\n    return layouts;\n  }\n}\n","/**\n * LayoutEngine.ts\n * Generates layouts for image cloud using placement strategies\n *\n * Public API:\n * - generateLayout(imageCount, containerBounds, options)\n * - getOriginalState(imageId)\n * - reset()\n * - updateConfig(newConfig)\n */\n\nimport type { LayoutConfig, ImageLayout, ContainerBounds, PlacementLayout, AdaptiveSizingResult, ImageConfig, FixedModeHeight, ResponsiveBreakpoints } from '../config/types';\nimport { RandomPlacementLayout } from '../layouts/RandomPlacementLayout';\nimport { RadialPlacementLayout } from '../layouts/RadialPlacementLayout';\nimport { GridPlacementLayout } from '../layouts/GridPlacementLayout';\nimport { SpiralPlacementLayout } from '../layouts/SpiralPlacementLayout';\nimport { ClusterPlacementLayout } from '../layouts/ClusterPlacementLayout';\nimport { WavePlacementLayout } from '../layouts/WavePlacementLayout';\nimport { HoneycombPlacementLayout } from '../layouts/HoneycombPlacementLayout';\n\nexport interface LayoutEngineConfig {\n  layout: LayoutConfig;\n  image: ImageConfig;\n}\n\nexport class LayoutEngine {\n  private config: LayoutConfig;\n  private imageConfig: ImageConfig;\n  private layouts: Map<number, ImageLayout>;\n  private placementLayout: PlacementLayout;\n\n  constructor(config: LayoutEngineConfig) {\n    this.config = config.layout;\n    this.imageConfig = config.image;\n\n    this.layouts = new Map();  // Store original states by image ID\n\n    // Initialize placement layout strategy\n    this.placementLayout = this.initLayout();\n  }\n\n  /**\n   * Initialize the appropriate placement layout based on config type\n   * @returns Initialized placement layout\n   */\n  private initLayout(): PlacementLayout {\n    switch (this.config.algorithm) {\n      case 'radial':\n        return new RadialPlacementLayout(this.config, this.imageConfig);\n      case 'grid':\n        return new GridPlacementLayout(this.config, this.imageConfig);\n      case 'spiral':\n        return new SpiralPlacementLayout(this.config, this.imageConfig);\n      case 'cluster':\n        return new ClusterPlacementLayout(this.config, this.imageConfig);\n      case 'wave':\n        return new WavePlacementLayout(this.config, this.imageConfig);\n      case 'honeycomb':\n        return new HoneycombPlacementLayout(this.config, this.imageConfig);\n      case 'random':\n      default:\n        return new RandomPlacementLayout(this.config, this.imageConfig);\n    }\n  }\n\n  /**\n   * Generate layout positions for images\n   * @param imageCount - Number of images to layout\n   * @param containerBounds - Container dimensions {width, height}\n   * @param options - Optional overrides for configuration (e.g. fixedHeight)\n   * @returns Array of layout objects with position, rotation, scale\n   */\n  generateLayout(imageCount: number, containerBounds: ContainerBounds, options: Partial<LayoutConfig> = {}): ImageLayout[] {\n    const layouts = this.placementLayout.generate(imageCount, containerBounds, options);\n\n    // Store layouts for state retrieval\n    layouts.forEach(layout => {\n      this.layouts.set(layout.id, layout);\n    });\n\n    return layouts;\n  }\n\n  /**\n   * Get the original layout state for an image\n   * @param imageId - The image ID (number or string)\n   * @returns Original layout state or undefined if not found\n   */\n  getOriginalState(imageId: number | string): ImageLayout | undefined {\n    return this.layouts.get(Number(imageId));\n  }\n\n  /**\n   * Reset all stored layouts\n   */\n  reset(): void {\n    this.layouts.clear();\n  }\n\n  /**\n   * Update config dynamically (useful for responsive changes)\n   * @param newConfig - Updated configuration\n   */\n  updateConfig(newConfig: Partial<LayoutEngineConfig>): void {\n    // Update layout config\n    if (newConfig.layout) {\n      Object.assign(this.config, newConfig.layout);\n\n      // Reinitialize placement layout if algorithm changed\n      if (newConfig.layout.algorithm && newConfig.layout.algorithm !== this.config.algorithm) {\n        this.placementLayout = this.initLayout();\n      }\n    }\n\n    // Update image config\n    if (newConfig.image) {\n      Object.assign(this.imageConfig, newConfig.image);\n    }\n  }\n\n  /**\n   * Get responsive breakpoints from layout config\n   */\n  private getBreakpoints(): ResponsiveBreakpoints {\n    return this.config.responsive ?? {\n      mobile: { maxWidth: 767 },\n      tablet: { maxWidth: 1199 }\n    };\n  }\n\n  /**\n   * Resolve breakpoint name based on viewport width\n   */\n  resolveBreakpoint(viewportWidth: number): 'mobile' | 'tablet' | 'screen' {\n    const breakpoints = this.getBreakpoints();\n\n    if (viewportWidth <= breakpoints.mobile.maxWidth) {\n      return 'mobile';\n    }\n    if (viewportWidth <= breakpoints.tablet.maxWidth) {\n      return 'tablet';\n    }\n    return 'screen';\n  }\n\n  /**\n   * Resolve the effective base height based on image config and current viewport\n   * @param viewportWidth - Current viewport width\n   * @returns Resolved base height or undefined if should auto-calculate (adaptive mode)\n   */\n  resolveBaseHeight(viewportWidth: number): number | undefined {\n    const sizing = this.imageConfig.sizing;\n\n    // If mode is adaptive (or not set), return undefined to signal auto-calculation\n    if (!sizing || sizing.mode === 'adaptive') {\n      return undefined;\n    }\n\n    // Fixed or responsive mode - use the height property\n    const height = sizing.height;\n\n    if (height === undefined) {\n      return undefined; // No height specified, fall back to adaptive\n    }\n\n    if (typeof height === 'number') {\n      return height;\n    }\n\n    // Responsive height for fixed mode\n    const responsiveHeight = height as FixedModeHeight;\n    const breakpoint = this.resolveBreakpoint(viewportWidth);\n\n    // Fallback chain: specific breakpoint -> higher breakpoints -> any defined\n    if (breakpoint === 'mobile') {\n      return responsiveHeight.mobile ?? responsiveHeight.tablet ?? responsiveHeight.screen;\n    }\n    if (breakpoint === 'tablet') {\n      return responsiveHeight.tablet ?? responsiveHeight.screen ?? responsiveHeight.mobile;\n    }\n    // screen\n    return responsiveHeight.screen ?? responsiveHeight.tablet ?? responsiveHeight.mobile;\n  }\n\n  /**\n   * Calculate adaptive image size based on container dimensions and image count\n   * @param containerBounds - Container dimensions {width, height}\n   * @param imageCount - Number of images to display\n   * @param maxHeight - Maximum height constraint (upper bound)\n   * @param viewportWidth - Current viewport width for baseHeight resolution\n   * @returns Calculated sizing result with height\n   */\n  calculateAdaptiveSize(\n    containerBounds: ContainerBounds,\n    imageCount: number,\n    maxHeight: number,\n    viewportWidth: number\n  ): AdaptiveSizingResult {\n    const sizing = this.imageConfig.sizing;\n\n    // Check if user specified a fixed height in image config\n    const userBaseHeight = this.resolveBaseHeight(viewportWidth);\n\n    // If user specified baseHeight (fixed/responsive mode), use it directly\n    // Don't clamp to maxHeight since user explicitly chose this value\n    if (userBaseHeight !== undefined) {\n      return { height: userBaseHeight };\n    }\n\n    // Adaptive mode - auto-calculate based on container and image count\n    const minSize = sizing?.minSize ?? 50;\n    const maxSize = sizing?.maxSize ?? 400;\n    const targetCoverage = this.config.targetCoverage ?? 0.6;\n    const densityFactor = this.config.densityFactor ?? 1.0;\n\n    const { width, height } = containerBounds;\n\n    // Calculate area-based optimal size\n    const containerArea = width * height;\n    const targetArea = containerArea * targetCoverage;\n    const areaPerImage = targetArea / imageCount;\n\n    // Calculate height from area assuming 1.4 aspect ratio (landscape images)\n    const aspectRatio = 1.4;\n    let calculatedHeight = Math.sqrt(areaPerImage / aspectRatio);\n\n    // Apply density factor\n    calculatedHeight *= densityFactor;\n\n    // Clamp to maximum height constraint\n    calculatedHeight = Math.min(calculatedHeight, maxHeight);\n\n    // Apply min/max constraints\n    let finalHeight = this.clamp(calculatedHeight, minSize, maxSize);\n\n    // 'minimize' behavior: force fit below minimum if needed (0.05 floor)\n    if (finalHeight === minSize && calculatedHeight < minSize) {\n      // Minimum floor is 5% of calculated size\n      const floor = Math.max(minSize * 0.05, 20);\n      finalHeight = Math.max(floor, calculatedHeight);\n    }\n\n    // Honeycomb: cap so the outermost ring stays within the container.\n    // Must apply here (not in generate()) so img.style.height matches tiling pitch.\n    if (this.config.algorithm === 'honeycomb') {\n      finalHeight = Math.min(finalHeight, this.honeycombMaxImageHeight(imageCount, containerBounds));\n    }\n\n    return { height: finalHeight };\n  }\n\n  /**\n   * Returns the largest image height at which all honeycomb rings fit within the container.\n   * Spacing is 0 for this calculation (user spacing is additive on top of the image height;\n   * any non-zero spacing only makes the constraint tighter).\n   */\n  private honeycombMaxImageHeight(imageCount: number, containerBounds: ContainerBounds): number {\n    if (imageCount <= 1) return Infinity;\n\n    // Find outermost ring: cells in rings 0..k = 1 + 3k(k+1)\n    let maxRing = 0;\n    let total = 1;\n    while (total < imageCount) {\n      maxRing++;\n      total += 6 * maxRing;\n    }\n\n    const padding = this.config.spacing?.padding ?? 50;\n    const spacing = this.config.honeycomb?.spacing ?? 0;\n    const containerCX = containerBounds.width / 2;\n    const containerCY = containerBounds.height / 2;\n\n    // colStep = √3/2 ≈ 0.866 * hexH; hex half-width = 1/√3 ≈ 0.577 * imageSize\n    const COL_STEP_RATIO = Math.sqrt(3) / 2;\n    const HALF_WIDTH_RATIO = 1 / Math.sqrt(3);\n\n    // Vertical: containerCY - maxRing*(size+spacing) - size/2 >= padding\n    const maxV = (containerCY - padding - spacing * maxRing) / (maxRing + 0.5);\n\n    // Horizontal: containerCX - COL_STEP_RATIO*maxRing*(size+spacing) - HALF_WIDTH_RATIO*size >= padding\n    const maxH = (containerCX - padding - COL_STEP_RATIO * spacing * maxRing)\n                 / (COL_STEP_RATIO * maxRing + HALF_WIDTH_RATIO);\n\n    return Math.max(10, Math.min(maxV, maxH));\n  }\n\n  /**\n   * Utility: Clamp a value between min and max\n   */\n  private clamp(value: number, min: number, max: number): number {\n    return Math.max(min, Math.min(max, value));\n  }\n}","/**\n * Type definitions for Image Gallery Library\n */\n\n// ============================================================================\n// Core Data Types\n// ============================================================================\n\nexport interface ImageLayout {\n  id: number;\n  x: number;\n  y: number;\n  rotation: number;\n  scale: number;\n  baseSize: number;\n  zIndex?: number;\n}\n\nexport interface ContainerBounds {\n  width: number;\n  height: number;\n}\n\nexport interface ResponsiveHeight {\n  minWidth: number;\n  height: number;\n}\n\nexport interface TransformParams {\n  x?: number;\n  y?: number;\n  rotation?: number;\n  scale?: number;\n}\n\n// ============================================================================\n// Loader Configuration\n// ============================================================================\n\n// === Static source types (shape-based, no 'type' discriminant) ===\n\nexport interface StaticUrlsSource {\n  urls: string[];\n}\n\nexport interface StaticPathSource {\n  path: string;\n  files: string[];\n}\n\nexport interface StaticJsonSource {\n  json: string;\n}\n\nexport type StaticSource = StaticUrlsSource | StaticPathSource | StaticJsonSource;\n\n// === Google Drive source types (shape-based) ===\n\nexport interface GoogleDriveFolderSource {\n  folders: string[];\n  recursive?: boolean;\n}\n\nexport interface GoogleDriveFilesSource {\n  files: string[];\n}\n\nexport type GoogleDriveSource = GoogleDriveFolderSource | GoogleDriveFilesSource;\n\n// === Loader configs (inner config objects) ===\n\nexport interface StaticLoaderInnerConfig {\n  sources: StaticSource[];\n  validateUrls?: boolean;\n  validationTimeout?: number;\n  validationMethod?: 'head' | 'simple' | 'none';\n  allowedExtensions?: string[];\n  debugLogging?: boolean;\n}\n\nexport interface GoogleDriveLoaderInnerConfig {\n  apiKey: string;\n  sources: GoogleDriveSource[];\n  apiEndpoint?: string;\n  allowedExtensions?: string[];\n  debugLogging?: boolean;\n}\n\n// === Loader entries (key-based identification) ===\n\nexport interface StaticLoaderEntry {\n  static: StaticLoaderInnerConfig;\n}\n\nexport interface GoogleDriveLoaderEntry {\n  googleDrive: GoogleDriveLoaderInnerConfig;\n}\n\nexport type LoaderEntry = StaticLoaderEntry | GoogleDriveLoaderEntry;\n\n// === Shared loader config ===\n\nexport interface SharedLoaderConfig {\n  validateUrls?: boolean;\n  validationTimeout?: number;\n  validationMethod?: 'head' | 'simple' | 'none';\n  allowedExtensions?: string[];\n}\n\n// === Debug configuration ===\n\nexport interface DebugConfig {\n  enabled?: boolean;    // General debug logging (was top-level `debug`)\n  centers?: boolean;    // Center position markers (was `layout.debugCenters`)\n  loaders?: boolean;    // Loader debug output (was `config.loaders.debugLogging`)\n}\n\n// === Config section ===\n\nexport interface ConfigSection {\n  loaders?: SharedLoaderConfig;\n  debug?: DebugConfig;\n}\n\n// ============================================================================\n// Image Configuration (sizing and rotation)\n// ============================================================================\n\n/**\n * Sizing mode:\n * - 'fixed': single explicit height for all breakpoints\n * - 'responsive': different heights per breakpoint (mobile/tablet/screen)\n * - 'adaptive': auto-calculates based on container size and image count\n */\nexport type SizingMode = 'fixed' | 'responsive' | 'adaptive';\n\n/**\n * Fixed mode height configuration with responsive breakpoints\n * At least one of mobile, tablet, or screen is required\n */\nexport interface FixedModeHeight {\n  mobile?: number;             // Height for mobile viewports (< responsive.mobile.maxWidth)\n  tablet?: number;             // Height for tablet viewports (< responsive.tablet.maxWidth)\n  screen?: number;             // Height for desktop/large screens (>= responsive.tablet.maxWidth)\n}\n\n/**\n * Legacy responsive base height configuration (kept for backward compatibility)\n */\nexport interface ResponsiveBaseHeight {\n  default: number;             // Base height for large screens\n  tablet?: number;             // Height for tablet\n  mobile?: number;             // Height for mobile\n}\n\n/**\n * Image variance configuration\n * Controls random size variation applied to images\n */\nexport interface ImageVarianceConfig {\n  min: number;                 // 0.25-1 (e.g., 0.8)\n  max: number;                 // 1-1.75 (e.g., 1.2)\n}\n\n/**\n * Image sizing configuration\n */\nexport interface ImageSizingConfig {\n  mode: SizingMode;                              // Required: 'fixed', 'responsive', or 'adaptive'\n  height?: number | FixedModeHeight;             // Fixed/responsive mode: explicit heights\n  minSize?: number;                              // Adaptive mode only: minimum image height (default: 50)\n  maxSize?: number;                              // Adaptive mode only: maximum image height (default: 400)\n  variance?: ImageVarianceConfig;                // Size variance (min: 0.25-1, max: 1-1.75)\n}\n\n/**\n * Image rotation mode\n */\nexport type ImageRotationMode = 'none' | 'random' | 'tangent';\n\n/**\n * Image rotation range configuration\n */\nexport interface ImageRotationRange {\n  min: number;                 // Negative degrees (-180 to 0)\n  max: number;                 // Positive degrees (0 to 180)\n}\n\n/**\n * Image rotation configuration\n */\nexport interface ImageRotationConfig {\n  mode: ImageRotationMode;     // default: 'none'\n  range?: ImageRotationRange;  // Range for random mode\n}\n\n/**\n * Combined image configuration\n */\nexport interface ImageConfig {\n  sizing?: ImageSizingConfig;\n  rotation?: ImageRotationConfig;\n}\n\n// ============================================================================\n// Layout Configuration\n// ============================================================================\n\nexport interface AdaptiveSizingResult {\n  height: number;                // Calculated image height\n}\n\n/**\n * Responsive breakpoints configuration for layout\n * Defines viewport width thresholds for mobile and tablet\n */\nexport interface ResponsiveBreakpoints {\n  mobile: { maxWidth: number };   // Default: 767\n  tablet: { maxWidth: number };   // Default: 1199\n  // screen is implicitly > tablet.maxWidth\n}\n\n// Legacy responsive height uses the existing ResponsiveHeight interface (minWidth, height)\n\nexport interface LayoutSpacingConfig {\n  padding: number;\n}\n\n// Legacy interface kept for backward compatibility in LayoutEngine\nexport interface LegacyLayoutRotationConfig {\n  enabled: boolean;\n  range: {\n    min: number;\n    max: number;\n  };\n}\n\n// ============================================================================\n// Algorithm-Specific Configuration\n// ============================================================================\n\nexport interface GridAlgorithmConfig {\n  columns: number | 'auto';\n  rows: number | 'auto';\n  stagger: 'none' | 'row' | 'column';\n  jitter: number;\n  overlap: number;\n  fillDirection: 'row' | 'column';\n  alignment: 'start' | 'center' | 'end';\n  gap: number;\n  overflowOffset: number;  // 0-0.5, percentage of cell size for stacking overflow images (default: 0.25)\n}\n\nexport interface RadialAlgorithmConfig {\n  tightness: number;  // Ring spacing multiplier (default: 1.0). Higher = more spread, lower = tighter.\n}\n\nexport interface SpiralAlgorithmConfig {\n  spiralType: 'golden' | 'archimedean' | 'logarithmic';\n  direction: 'clockwise' | 'counterclockwise';\n  tightness: number;\n  scaleDecay: number;\n  startAngle: number;\n}\n\nexport interface ClusterAlgorithmConfig {\n  clusterCount: number | 'auto';\n  clusterSpread: number;\n  clusterSpacing: number;\n  density: 'uniform' | 'varied';\n  overlap: number;\n  distribution: 'gaussian' | 'uniform';\n}\n\nexport interface WaveAlgorithmConfig {\n  rows: number;\n  amplitude: number;\n  frequency: number;\n  phaseShift: number;\n  synchronization: 'offset' | 'synchronized' | 'alternating';\n  // Note: Image rotation along wave is now controlled via image.rotation.mode = 'tangent'\n}\n\nexport type LayoutAlgorithm = 'random' | 'radial' | 'grid' | 'spiral' | 'cluster' | 'wave' | 'honeycomb';\n\nexport interface HoneycombAlgorithmConfig {\n  spacing?: number;  // extra gap in pixels beyond edge-to-edge (default: 0)\n}\n\nexport interface LayoutConfig {\n  algorithm: LayoutAlgorithm;\n  spacing: LayoutSpacingConfig;\n  scaleDecay?: number;           // For Radial/Spiral - progressive size reduction (0-1, default: 0)\n  responsive?: ResponsiveBreakpoints;  // Viewport width breakpoints (mobile/tablet)\n  targetCoverage?: number;       // 0-1, for adaptive sizing (default: 0.6)\n  densityFactor?: number;        // Controls center point spacing (default: 1.0)\n  radial?: RadialAlgorithmConfig;\n  grid?: GridAlgorithmConfig;\n  spiral?: SpiralAlgorithmConfig;\n  cluster?: ClusterAlgorithmConfig;\n  wave?: WaveAlgorithmConfig;\n  honeycomb?: HoneycombAlgorithmConfig;\n}\n\n// ============================================================================\n// Animation Configuration\n// ============================================================================\n\n// Entry Path Types\nexport type EntryPathType = 'linear' | 'arc' | 'bounce' | 'elastic' | 'wave';\n\nexport interface BouncePathConfig {\n  overshoot: number;        // 0.1-0.3, how far past target (default: 0.15)\n  bounces: 1 | 2 | 3;       // number of bounces before settling (default: 1)\n  decayRatio: number;       // 0.3-0.7, each bounce is this % of previous (default: 0.5)\n}\n\nexport type BouncePreset = 'energetic' | 'playful' | 'subtle';\n\nexport interface ElasticPathConfig {\n  stiffness: number;        // 100-500, higher = faster oscillation (default: 200)\n  damping: number;          // 10-50, higher = fewer oscillations (default: 20)\n  mass: number;             // 0.5-3, higher = slower, more momentum (default: 1)\n  oscillations: number;     // 2-5, visible oscillation count (default: 3)\n}\n\nexport type ElasticPreset = 'gentle' | 'bouncy' | 'wobbly' | 'snappy';\n\nexport interface WavePathConfig {\n  amplitude: number;        // 20-100px, wave height (default: 40)\n  frequency: number;        // 1-4, number of complete waves (default: 2)\n  decay: boolean;           // true = wave diminishes toward target (default: true)\n  decayRate: number;        // 0.5-1, how fast amplitude decreases (default: 0.8)\n  phase: number;            // 0-2π, starting phase offset (default: 0)\n}\n\nexport type WavePathPreset = 'gentle' | 'playful' | 'serpentine' | 'flutter';\n\nexport interface EntryPathConfig {\n  type: EntryPathType;\n  // Preset shortcuts (type-specific)\n  bouncePreset?: BouncePreset;\n  elasticPreset?: ElasticPreset;\n  wavePreset?: WavePathPreset;\n  // Type-specific detailed configs (override presets)\n  bounce?: Partial<BouncePathConfig>;\n  elastic?: Partial<ElasticPathConfig>;\n  wave?: Partial<WavePathConfig>;\n}\n\n// Entry Rotation Types\nexport type EntryRotationMode = 'none' | 'settle' | 'spin' | 'wobble' | 'random';\n\nexport interface EntryRotationConfig {\n  mode: EntryRotationMode;\n  startRotation?: number | { min: number; max: number };  // For 'settle' mode\n  spinCount?: number;                                      // For 'spin' mode (default: 1)\n  direction?: 'clockwise' | 'counterclockwise' | 'auto' | 'random';  // For 'spin' mode\n  wobble?: {\n    amplitude: number;    // degrees of oscillation (default: 15)\n    frequency: number;    // oscillations during animation (default: 3)\n    decay: boolean;       // whether oscillation diminishes (default: true)\n  };\n}\n\n// Entry Scale Types\nexport type EntryScaleMode = 'none' | 'grow' | 'shrink' | 'pop' | 'random';\n\nexport interface EntryScalePopConfig {\n  overshoot: number;      // How much to overshoot final scale (1.1-1.5, default: 1.2)\n  bounces: number;        // Number of bounces before settling (1-3, default: 1)\n}\n\nexport interface EntryScaleConfig {\n  mode: EntryScaleMode;\n  startScale?: number;                        // Fixed start scale for grow/shrink (0.1-4.0)\n  range?: { min: number; max: number };       // Random range for 'random' mode\n  pop?: EntryScalePopConfig;                  // Config for 'pop' mode\n}\n\nexport type EntryStartPosition =\n  | 'nearest-edge'\n  | 'top'\n  | 'bottom'\n  | 'left'\n  | 'right'\n  | 'center'\n  | 'random-edge'\n  | 'circular';\n\nexport interface EntryCircularConfig {\n  radius?: number | string;  // pixels or percentage like '120%', default: '120%' of container diagonal\n  distribution?: 'even' | 'random';  // default: 'even'\n}\n\nexport interface EntryStartConfig {\n  position: EntryStartPosition;\n  offset?: number;  // pixels beyond edge, default: 100\n  circular?: EntryCircularConfig;\n}\n\nexport interface EntryTimingConfig {\n  duration: number;  // default: 600ms\n}\n\nexport interface EntryAnimationConfig {\n  start: EntryStartConfig;\n  timing: EntryTimingConfig;\n  easing: string;  // CSS easing, default: 'cubic-bezier(0.25, 1, 0.5, 1)'\n  path?: EntryPathConfig;  // Animation path type (linear, bounce, elastic, wave)\n  rotation?: EntryRotationConfig;  // Entry rotation animation\n  scale?: EntryScaleConfig;  // Entry scale animation\n}\n\nexport interface AnimationEasingConfig {\n  default: string;\n  bounce: string;\n  focus: string;\n}\n\nexport interface AnimationQueueConfig {\n  enabled: boolean;\n  interval: number;\n}\n\nexport interface AnimationConfig {\n  duration: number;\n  easing: AnimationEasingConfig;\n  queue: AnimationQueueConfig;\n  entry?: EntryAnimationConfig;\n  idle?: IdleAnimationConfig;\n}\n\n// ============================================================================\n// Idle Animation Configuration\n// ============================================================================\n\nexport type IdleAnimationType = 'wiggle' | 'pulse' | 'blink' | 'spin' | 'custom' | 'none';\nexport type IdleSyncMode = 'together' | 'random';\n\nexport interface IdleWiggleConfig {\n  maxAngle: number;\n  speed: number;\n  sync: IdleSyncMode;\n}\n\nexport interface IdlePulseConfig {\n  minScale: number;\n  maxScale: number;\n  speed: number;\n  sync: IdleSyncMode;\n}\n\nexport interface IdleBlinkConfig {\n  onRatio: number;\n  speed: number;\n  style: 'snap' | 'fade';\n}\n\nexport interface IdleSpinConfig {\n  speed: number;\n  direction: 'clockwise' | 'counterclockwise';\n}\n\nexport interface IdleCustomContext {\n  element: HTMLElement;\n  index: number;\n  totalImages: number;\n}\n\nexport type IdleCustomAnimationFn = (ctx: IdleCustomContext) => Animation | (() => void);\n\nexport interface IdleAnimationConfig {\n  type: IdleAnimationType;\n  wiggle?: IdleWiggleConfig;\n  pulse?: IdlePulseConfig;\n  blink?: IdleBlinkConfig;\n  spin?: IdleSpinConfig;\n  custom?: IdleCustomAnimationFn;\n  startDelay?: number;\n}\n\n// ============================================================================\n// Interaction Configuration\n// ============================================================================\n\nexport interface FocusInteractionConfig {\n  scalePercent: number;           // Percentage of container (0-1 as fraction, 1-100 as percent)\n  zIndex: number;\n  animationDuration?: number;\n}\n\nexport interface NavigationInteractionConfig {\n  keyboard?: boolean;\n  swipe?: boolean;\n  mouseWheel?: boolean;\n}\n\nexport interface InteractionConfig {\n  focus: FocusInteractionConfig;\n  navigation?: NavigationInteractionConfig;\n  dragging?: boolean;\n}\n\n// ============================================================================\n// UI Configuration\n// ============================================================================\n\nexport interface UIConfig {\n  showLoadingSpinner: boolean;\n  showImageCounter?: boolean;\n  loadingElement?: string | HTMLElement;\n  errorElement?: string | HTMLElement;\n  counterElement?: string | HTMLElement;\n  showNavButtons?: boolean;\n  prevButtonElement?: string | HTMLElement;\n  nextButtonElement?: string | HTMLElement;\n  showFocusOutline?: boolean;\n}\n\n// ============================================================================\n// Main Gallery Configuration\n// ============================================================================\n\nexport interface ImageCloudConfig {\n  loaders: LoaderEntry[];\n  config: ConfigSection;\n  image: ImageConfig;\n  layout: LayoutConfig;\n  animation: AnimationConfig;\n  interaction: InteractionConfig;\n  ui: UIConfig;\n  styling?: ImageStylingConfig;\n}\n\n// Backwards compatibility alias\nexport type GalleryConfig = ImageCloudConfig;\n\nexport interface ImageCloudOptions {\n  container?: string | HTMLElement;\n  images?: string[];\n  loaders?: LoaderEntry[];\n  config?: ConfigSection;\n  image?: Partial<ImageConfig>;\n  layout?: Partial<LayoutConfig>;\n  animation?: Partial<AnimationConfig>;\n  interaction?: Partial<InteractionConfig>;\n  ui?: Partial<UIConfig>;\n  /** @deprecated Use `ui` instead of `rendering.ui` */\n  rendering?: { ui?: Partial<UIConfig> };\n  styling?: Partial<ImageStylingConfig>;\n}\n\n// Backwards compatibility alias\nexport type ImageGalleryOptions = ImageCloudOptions;\n\n// ============================================================================\n// Legacy Configuration Types (for backward compatibility)\n// ============================================================================\n\nexport interface LegacyLayoutConfig {\n  baseImageSize?: number;\n  rotationRange?: { min: number; max: number };\n  type?: 'random' | 'radial';\n  sizeVarianceMin?: number;\n  sizeVarianceMax?: number;\n  responsiveHeights?: ResponsiveHeight[];\n  padding?: number;\n  minSpacing?: number;\n  minRotation?: number;\n  maxRotation?: number;\n}\n\nexport interface LegacyAnimationConfig {\n  duration?: number;\n  queueInterval?: number;\n  easing?: string;\n  bounceEasing?: string;\n}\n\nexport interface LegacyZoomConfig {\n  focusScale?: number;\n  mobileScale?: number;     // Deprecated: use scaleTo/scalePercent instead\n  focusZIndex?: number;\n}\n\nexport interface LegacyConfig {\n  layout?: LegacyLayoutConfig;\n  animation?: LegacyAnimationConfig;\n  zoom?: LegacyZoomConfig;\n  googleDrive?: {\n    apiKey?: string;\n    apiEndpoint?: string;\n    imageExtensions?: string[];\n  };\n  breakpoints?: {\n    mobile?: number;\n    tablet?: number;\n    desktop?: number;\n  };\n  isMobile?: () => boolean;\n  ui?: {\n    showLoadingSpinner?: boolean;\n  };\n}\n\nexport interface LegacyImageGalleryOptions {\n  containerId?: string;\n  loaderType?: 'googleDrive' | 'static';\n  folderUrl?: string;\n  googleDrive?: {\n    apiKey?: string;\n  };\n}\n\n// ============================================================================\n// Interface Dependencies\n// ============================================================================\n\nexport interface PlacementLayout {\n  generate(\n    imageCount: number,\n    containerBounds: ContainerBounds,\n    options?: Partial<LayoutConfig>\n  ): ImageLayout[];\n}\n\n/**\n * ImageFilter interface for filtering images by extension\n * Implemented by the ImageFilter class in loaders/ImageFilter.ts\n */\nexport interface IImageFilter {\n  isAllowed(filename: string): boolean;\n  getAllowedExtensions(): string[];\n}\n\n/**\n * ImageLoader interface with consistent lifecycle pattern:\n * 1. Constructor - Initialize with required parameters, throw if missing\n * 2. prepare(filter) - Async discovery of images, accepts filter\n * 3. imagesLength() - Return count of images (after prepare)\n * 4. imageURLs() - Return ordered list of URLs (after prepare)\n */\nexport interface ImageLoader {\n  /**\n   * Async preparation - discovers images and applies filter\n   * Succeeds even if 0 images found (gallery handles empty state)\n   * @param filter - Filter to apply to discovered images\n   */\n  prepare(filter: IImageFilter): Promise<void>;\n\n  /**\n   * Get the number of discovered images\n   * @throws Error if called before prepare() completes\n   */\n  imagesLength(): number;\n\n  /**\n   * Get the ordered list of image URLs\n   * @throws Error if called before prepare() completes\n   */\n  imageURLs(): string[];\n\n  /**\n   * Check if the loader has been prepared\n   */\n  isPrepared(): boolean;\n}\n\nexport interface GoogleDriveFile {\n  id: string;\n  name: string;\n  mimeType: string;\n  parents?: string[];\n}\n\nexport interface GoogleDriveResponse {\n  files: GoogleDriveFile[];\n  nextPageToken?: string;\n}\n\nexport type DeepPartial<T> = {\n  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];\n};\n\n// ============================================================================\n// Image Styling Configuration\n// ============================================================================\n\nexport type ShadowPreset = 'none' | 'sm' | 'md' | 'lg' | 'glow';\n\nexport type BorderStyle =\n  | 'solid'      // Most common - continuous line\n  | 'dashed'     // Series of dashes\n  | 'dotted'     // Series of dots\n  | 'double'     // Two parallel lines\n  | 'none'       // No border\n  | 'groove'     // 3D carved into page\n  | 'ridge'      // 3D raised from page\n  | 'inset'      // 3D embedded look\n  | 'outset'     // 3D raised look\n  | 'hidden';    // Same as none (for table border conflict resolution)\n\nexport interface BorderConfig {\n  width?: number;             // pixels, default: 0\n  color?: string;             // CSS color, default: '#000'\n  radius?: number;            // pixels, default: 8\n  style?: BorderStyle;        // default: 'solid'\n}\n\nexport interface DropShadowConfig {\n  x: number;\n  y: number;\n  blur: number;\n  color: string;\n}\n\nexport interface FilterConfig {\n  grayscale?: number;    // 0-1\n  blur?: number;         // pixels\n  brightness?: number;   // multiplier (1 = normal)\n  contrast?: number;     // multiplier\n  saturate?: number;     // multiplier\n  opacity?: number;      // 0-1\n  sepia?: number;        // 0-1\n  hueRotate?: number;    // degrees\n  invert?: number;       // 0-1\n  dropShadow?: DropShadowConfig | string;\n}\n\nexport interface OutlineConfig {\n  width?: number;        // pixels\n  color?: string;        // CSS color\n  style?: BorderStyle;   // reuses BorderStyle type\n  offset?: number;       // pixels\n}\n\nexport type ClipPathShape =\n  | 'circle'\n  | 'square'\n  | 'triangle'\n  | 'pentagon'\n  | 'hexagon'\n  | 'octagon'\n  | 'diamond';\n\nexport type ClipPathMode = 'percent' | 'height-relative';\n\nexport interface ClipPathConfig {\n  shape: ClipPathShape;\n  mode?: ClipPathMode;  // defaults to 'percent'\n}\n\nexport interface ImageStyleState {\n  // CSS class names (space-separated string or array)\n  className?: string | string[];\n\n  // Border (shorthand applies to all sides)\n  border?: BorderConfig;\n  borderTop?: Partial<BorderConfig>;\n  borderRight?: Partial<BorderConfig>;\n  borderBottom?: Partial<BorderConfig>;\n  borderLeft?: Partial<BorderConfig>;\n\n  // Per-corner border radius (overrides border.radius for specific corners)\n  borderRadiusTopLeft?: number;\n  borderRadiusTopRight?: number;\n  borderRadiusBottomRight?: number;\n  borderRadiusBottomLeft?: number;\n\n  // Shadow (preset name or custom CSS string)\n  shadow?: ShadowPreset | string;\n\n  // Filters\n  filter?: FilterConfig;\n\n  // Other properties\n  opacity?: number;           // 0-1\n  cursor?: string;            // CSS cursor value\n  outline?: OutlineConfig;\n  objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';\n  aspectRatio?: string;       // e.g., '16/9', '1/1'\n  clipPath?: ClipPathShape | string | ClipPathConfig;  // Predefined shape, custom clip-path string, or config object\n}\n\nexport interface ImageStylingConfig {\n  default?: ImageStyleState;\n  hover?: Partial<ImageStyleState>;    // inherits from default\n  focused?: Partial<ImageStyleState>;  // inherits from default\n}\n\n// ============================================================================\n// Focus Animation Types (Cross-Animation Support)\n// ============================================================================\n\n/**\n * State machine states for zoom/focus animations\n */\nexport enum ZoomState {\n  IDLE = 'idle',                     // No focus, no animations\n  FOCUSING = 'focusing',             // Single image animating in\n  FOCUSED = 'focused',               // Stable focused state\n  UNFOCUSING = 'unfocusing',         // Single image animating out\n  CROSS_ANIMATING = 'cross_animating' // Two images: one out, one in\n}\n\n/**\n * Handle for a cancellable animation using Web Animations API\n */\nexport interface AnimationHandle {\n  id: string;\n  element: HTMLElement;\n  animation: Animation;\n  fromState: TransformParams;\n  toState: TransformParams;\n  startTime: number;\n  duration: number;\n}\n\n/**\n * Snapshot of an element's current transform state\n * Used for capturing position mid-animation\n */\nexport interface AnimationSnapshot {\n  x: number;\n  y: number;\n  rotation: number;\n  scale: number;\n}\n\n/**\n * Tracks an image that is currently animating\n */\nexport interface AnimatingImage {\n  element: HTMLElement;\n  originalState: ImageLayout;\n  animationHandle: AnimationHandle;\n  direction: 'in' | 'out';\n  originalWidth?: number;\n  originalHeight?: number;\n}","/**\n * Maps predefined shape names to CSS clip-path values.\n * Supports both percentage-based (responsive) and height-relative (aspect-ratio aware) modes.\n */\n\nimport type { ClipPathShape } from '../config/types';\nimport { HEXAGON_REF_HEIGHT, HEXAGON_REF_POINTS } from './hexagonGeometry';\n\nconst CLIP_PATH_SHAPES: Record<ClipPathShape, string> = {\n  // Geometric shapes - uses percentages for responsive sizing\n  circle: 'circle(50%)',\n  square: 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)',\n  triangle: 'polygon(50% 0%, 100% 100%, 0% 100%)',\n  pentagon: 'polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)',\n  hexagon: 'polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%)',\n  octagon: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)',\n  diamond: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)'\n};\n\n/**\n * Height-relative shapes: normalized at a reference height of 100px\n * Points are in pixels relative to the reference height\n * When applied, coordinates scale based on actual image height\n */\ninterface HeightRelativeShape {\n  refHeight: number;\n  points: Array<[number, number]>;  // [x, y] coordinates\n}\n\nconst CLIP_PATH_SHAPES_HEIGHT_RELATIVE: Record<ClipPathShape, HeightRelativeShape> = {\n  // Circle - uses radius in pixels (refHeight of 100px = 50px radius)\n  circle: {\n    refHeight: 100,\n    points: []  // Special case: handled separately\n  },\n  // Square - maintains perfect aspect ratio (always 1:1)\n  square: {\n    refHeight: 100,\n    points: [[0, 0], [100, 0], [100, 100], [0, 100]]\n  },\n  // Triangle - isosceles triangle\n  triangle: {\n    refHeight: 100,\n    points: [[50, 0], [100, 100], [0, 100]]\n  },\n  // Pentagon - regular pentagon\n  pentagon: {\n    refHeight: 100,\n    points: [[50, 0], [100, 38], [82, 100], [18, 100], [0, 38]]\n  },\n  // Hexagon - regular hexagon (reference points imported from hexagonGeometry)\n  hexagon: {\n    refHeight: HEXAGON_REF_HEIGHT,\n    points: HEXAGON_REF_POINTS\n  },\n  // Octagon - regular octagon\n  octagon: {\n    refHeight: 100,\n    points: [[30, 0], [70, 0], [100, 30], [100, 70], [70, 100], [30, 100], [0, 70], [0, 30]]\n  },\n  // Diamond - 45-degree rotated square\n  diamond: {\n    refHeight: 100,\n    points: [[50, 0], [100, 50], [50, 100], [0, 50]]\n  }\n};\n\n/**\n * Resolves a shape name or custom clip-path string to a valid CSS clip-path value.\n * @param shape - Predefined shape name or custom clip-path string\n * @returns Valid CSS clip-path value\n */\nexport function getClipPath(shape: ClipPathShape | string | undefined): string | undefined {\n  if (!shape) return undefined;\n\n  // Check if it's a predefined shape\n  if (shape in CLIP_PATH_SHAPES) {\n    return CLIP_PATH_SHAPES[shape as ClipPathShape];\n  }\n\n  // Treat as custom clip-path string (e.g., 'polygon(...)' or 'inset(...)')\n  return shape;\n}\n\n/**\n * Returns available predefined shape names for UI/documentation.\n */\nexport function getAvailableShapes(): ClipPathShape[] {\n  return Object.keys(CLIP_PATH_SHAPES) as ClipPathShape[];\n}\n\n/**\n * Calculates height-relative clip-path string for a given shape and image dimensions.\n * Scales the reference shape definition by (imageHeight / refHeight) and centers it horizontally.\n * @param shape - Predefined shape name\n * @param imageHeight - Actual image height in pixels\n * @param imageWidth - Actual image width in pixels (used to center the shape horizontally)\n * @returns CSS clip-path value (e.g., 'circle(50px)' or 'polygon(...)')\n */\nexport function calculateHeightRelativeClipPath(shape: ClipPathShape, imageHeight: number, imageWidth?: number): string {\n  const shapeDef = CLIP_PATH_SHAPES_HEIGHT_RELATIVE[shape];\n  if (!shapeDef) return '';\n\n  const scale = imageHeight / shapeDef.refHeight;\n\n  // Special case: circle uses circle() function with radius\n  if (shape === 'circle') {\n    const radius = Math.round(50 * scale * 100) / 100; // Round to 2 decimals\n    return `circle(${radius}px)`;\n  }\n\n  // Calculate offsets to center the shape's bounding box within the image.\n  // Compute the bounding box from the actual points (handles non-square shapes\n  // like the regular hexagon whose width ≠ refHeight).\n  const xs = shapeDef.points.map(([x]) => x);\n  const ys = shapeDef.points.map(([, y]) => y);\n  const shapeCenterX = ((Math.min(...xs) + Math.max(...xs)) / 2) * scale;\n  const shapeCenterY = ((Math.min(...ys) + Math.max(...ys)) / 2) * scale;\n\n  // Fallback imageWidth: use the scaled bounding box width when unknown\n  const scaledBBoxWidth = (Math.max(...xs) - Math.min(...xs)) * scale;\n\n  // Image's center\n  const imageCenterX = (imageWidth ?? scaledBBoxWidth) / 2;\n  const imageCenterY = imageHeight / 2;\n\n  // Offsets to move shape center to image center\n  const horizontalOffset = imageCenterX - shapeCenterX;\n  const verticalOffset = imageCenterY - shapeCenterY;\n\n  // For polygon shapes, scale all points and format as polygon()\n  const scaledPoints = shapeDef.points.map(([x, y]) => {\n    const scaledX = Math.round((x * scale + horizontalOffset) * 100) / 100;\n    const scaledY = Math.round((y * scale + verticalOffset) * 100) / 100;\n    return `${scaledX}px ${scaledY}px`;\n  });\n\n  return `polygon(${scaledPoints.join(', ')})`;\n}\n","/**\n * Style utilities for image styling configuration\n */\n\nimport type { ImageStyleState, FilterConfig, BorderConfig, ShadowPreset, DropShadowConfig, ClipPathConfig, ClipPathShape } from '../config/types';\nimport { SHADOW_PRESETS } from '../config/defaults';\nimport { getClipPath, calculateHeightRelativeClipPath } from './clipPathGenerator';\n\n/**\n * Check if a value is a known shadow preset\n */\nfunction isShadowPreset(value: string): value is ShadowPreset {\n  return value in SHADOW_PRESETS;\n}\n\n/**\n * Resolve shadow value - converts preset names to CSS values,\n * passes custom CSS strings through directly\n */\nexport function resolveShadow(shadow: ShadowPreset | string | undefined): string {\n  if (!shadow) return SHADOW_PRESETS.md;\n  if (isShadowPreset(shadow)) return SHADOW_PRESETS[shadow];\n  return shadow; // Custom CSS string\n}\n\n/**\n * Build CSS filter string from FilterConfig\n */\nexport function buildFilterString(filter: FilterConfig | undefined): string {\n  if (!filter) return '';\n\n  const parts: string[] = [];\n\n  if (filter.grayscale !== undefined) {\n    parts.push(`grayscale(${filter.grayscale})`);\n  }\n  if (filter.blur !== undefined) {\n    parts.push(`blur(${filter.blur}px)`);\n  }\n  if (filter.brightness !== undefined) {\n    parts.push(`brightness(${filter.brightness})`);\n  }\n  if (filter.contrast !== undefined) {\n    parts.push(`contrast(${filter.contrast})`);\n  }\n  if (filter.saturate !== undefined) {\n    parts.push(`saturate(${filter.saturate})`);\n  }\n  if (filter.opacity !== undefined) {\n    parts.push(`opacity(${filter.opacity})`);\n  }\n  if (filter.sepia !== undefined) {\n    parts.push(`sepia(${filter.sepia})`);\n  }\n  if (filter.hueRotate !== undefined) {\n    parts.push(`hue-rotate(${filter.hueRotate}deg)`);\n  }\n  if (filter.invert !== undefined) {\n    parts.push(`invert(${filter.invert})`);\n  }\n  if (filter.dropShadow !== undefined) {\n    if (typeof filter.dropShadow === 'string') {\n      parts.push(`drop-shadow(${filter.dropShadow})`);\n    } else {\n      const ds = filter.dropShadow as DropShadowConfig;\n      parts.push(`drop-shadow(${ds.x}px ${ds.y}px ${ds.blur}px ${ds.color})`);\n    }\n  }\n\n  return parts.join(' ');\n}\n\n/**\n * Build border CSS for a single side\n */\nfunction buildSingleBorder(config: BorderConfig | undefined): string {\n  if (!config || config.style === 'none' || config.width === 0) {\n    return 'none';\n  }\n  const width = config.width ?? 0;\n  const style = config.style ?? 'solid';\n  const color = config.color ?? '#000000';\n  return `${width}px ${style} ${color}`;\n}\n\n/**\n * CSS properties object type for style application\n */\nexport interface StyleProperties {\n  borderRadius?: string;\n  borderTopLeftRadius?: string;\n  borderTopRightRadius?: string;\n  borderBottomRightRadius?: string;\n  borderBottomLeftRadius?: string;\n  border?: string;\n  borderTop?: string;\n  borderRight?: string;\n  borderBottom?: string;\n  borderLeft?: string;\n  boxShadow?: string;\n  filter?: string;\n  opacity?: string;\n  cursor?: string;\n  outline?: string;\n  outlineOffset?: string;\n  objectFit?: string;\n  aspectRatio?: string;\n  clipPath?: string;\n  overflow?: string;\n}\n\n/**\n * Build complete style properties object from ImageStyleState\n * @param state - Image style state configuration\n * @param imageHeight - Optional image height for height-relative clip-path calculations\n * @param imageWidth - Optional image width for centering height-relative clip-path shapes\n */\nexport function buildStyleProperties(state: ImageStyleState | undefined, imageHeight?: number, imageWidth?: number): StyleProperties {\n  if (!state) return {};\n\n  const styles: StyleProperties = {};\n\n  // Border radius - check for per-corner overrides first\n  const hasPerCornerRadius = state.borderRadiusTopLeft !== undefined ||\n                             state.borderRadiusTopRight !== undefined ||\n                             state.borderRadiusBottomRight !== undefined ||\n                             state.borderRadiusBottomLeft !== undefined;\n\n  if (hasPerCornerRadius) {\n    // Use per-corner radius with fallback to base radius\n    const baseRadius = state.border?.radius ?? 0;\n    if (state.borderRadiusTopLeft !== undefined) {\n      styles.borderTopLeftRadius = `${state.borderRadiusTopLeft}px`;\n    } else if (baseRadius) {\n      styles.borderTopLeftRadius = `${baseRadius}px`;\n    }\n    if (state.borderRadiusTopRight !== undefined) {\n      styles.borderTopRightRadius = `${state.borderRadiusTopRight}px`;\n    } else if (baseRadius) {\n      styles.borderTopRightRadius = `${baseRadius}px`;\n    }\n    if (state.borderRadiusBottomRight !== undefined) {\n      styles.borderBottomRightRadius = `${state.borderRadiusBottomRight}px`;\n    } else if (baseRadius) {\n      styles.borderBottomRightRadius = `${baseRadius}px`;\n    }\n    if (state.borderRadiusBottomLeft !== undefined) {\n      styles.borderBottomLeftRadius = `${state.borderRadiusBottomLeft}px`;\n    } else if (baseRadius) {\n      styles.borderBottomLeftRadius = `${baseRadius}px`;\n    }\n  } else if (state.border?.radius !== undefined) {\n    styles.borderRadius = `${state.border.radius}px`;\n  }\n\n  // Check if any per-side border is defined\n  const hasPerSideBorder = state.borderTop || state.borderRight || state.borderBottom || state.borderLeft;\n\n  if (hasPerSideBorder) {\n    // Merge base border with per-side overrides\n    const baseBorder = state.border || {};\n\n    const topBorder = { ...baseBorder, ...state.borderTop };\n    const rightBorder = { ...baseBorder, ...state.borderRight };\n    const bottomBorder = { ...baseBorder, ...state.borderBottom };\n    const leftBorder = { ...baseBorder, ...state.borderLeft };\n\n    styles.borderTop = buildSingleBorder(topBorder);\n    styles.borderRight = buildSingleBorder(rightBorder);\n    styles.borderBottom = buildSingleBorder(bottomBorder);\n    styles.borderLeft = buildSingleBorder(leftBorder);\n  } else if (state.border) {\n    // Apply uniform border\n    styles.border = buildSingleBorder(state.border);\n  }\n\n  // Shadow\n  if (state.shadow !== undefined) {\n    styles.boxShadow = resolveShadow(state.shadow);\n  }\n\n  // Filter - always set to ensure hover filters are properly cleared on mouseleave\n  const filterStr = buildFilterString(state.filter);\n  styles.filter = filterStr || 'none';\n\n  // Opacity\n  if (state.opacity !== undefined) {\n    styles.opacity = String(state.opacity);\n  }\n\n  // Cursor\n  if (state.cursor !== undefined) {\n    styles.cursor = state.cursor;\n  }\n\n  // Outline\n  if (state.outline && state.outline.style !== 'none' && (state.outline.width ?? 0) > 0) {\n    const width = state.outline.width ?? 0;\n    const style = state.outline.style ?? 'solid';\n    const color = state.outline.color ?? '#000000';\n    styles.outline = `${width}px ${style} ${color}`;\n    if (state.outline.offset !== undefined) {\n      styles.outlineOffset = `${state.outline.offset}px`;\n    }\n  }\n\n  // Object fit\n  if (state.objectFit !== undefined) {\n    styles.objectFit = state.objectFit;\n  }\n\n  // Aspect ratio\n  if (state.aspectRatio !== undefined) {\n    styles.aspectRatio = state.aspectRatio;\n  }\n\n  // Clip path (cropping)\n  if (state.clipPath !== undefined) {\n    let clipPathValue: string | undefined;\n\n    // Check if clipPath is a config object with height-relative mode\n    const isConfig = typeof state.clipPath === 'object' && state.clipPath !== null && 'shape' in state.clipPath;\n    const config = isConfig ? (state.clipPath as ClipPathConfig) : undefined;\n\n    if (config?.mode === 'height-relative' && imageHeight) {\n      // Use height-relative calculation if mode is specified and imageHeight is available\n      clipPathValue = calculateHeightRelativeClipPath(config.shape, imageHeight, imageWidth);\n    } else {\n      // Fall back to standard clip-path resolution\n      const clipPathInput = isConfig && config ? config.shape : state.clipPath;\n      clipPathValue = getClipPath(clipPathInput as ClipPathShape | string);\n    }\n\n    if (clipPathValue) {\n      // When 'none' is specified, use 'unset' to clear any inherited clip-path\n      if (clipPathValue === 'none') {\n        styles.clipPath = 'unset';\n      } else {\n        styles.clipPath = clipPathValue;\n        styles.overflow = 'hidden';  // Ensure clean boundaries\n      }\n    }\n  }\n\n  return styles;\n}\n\n/**\n * Apply style properties to an HTML element\n */\nexport function applyStylesToElement(element: HTMLElement, styles: StyleProperties): void {\n  if (styles.borderRadius !== undefined) element.style.borderRadius = styles.borderRadius;\n  if (styles.borderTopLeftRadius !== undefined) element.style.borderTopLeftRadius = styles.borderTopLeftRadius;\n  if (styles.borderTopRightRadius !== undefined) element.style.borderTopRightRadius = styles.borderTopRightRadius;\n  if (styles.borderBottomRightRadius !== undefined) element.style.borderBottomRightRadius = styles.borderBottomRightRadius;\n  if (styles.borderBottomLeftRadius !== undefined) element.style.borderBottomLeftRadius = styles.borderBottomLeftRadius;\n  if (styles.border !== undefined) element.style.border = styles.border;\n  if (styles.borderTop !== undefined) element.style.borderTop = styles.borderTop;\n  if (styles.borderRight !== undefined) element.style.borderRight = styles.borderRight;\n  if (styles.borderBottom !== undefined) element.style.borderBottom = styles.borderBottom;\n  if (styles.borderLeft !== undefined) element.style.borderLeft = styles.borderLeft;\n  if (styles.boxShadow !== undefined) element.style.boxShadow = styles.boxShadow;\n  if (styles.filter !== undefined) element.style.filter = styles.filter;\n  if (styles.opacity !== undefined) element.style.opacity = styles.opacity;\n  if (styles.cursor !== undefined) element.style.cursor = styles.cursor;\n  if (styles.outline !== undefined) element.style.outline = styles.outline;\n  if (styles.outlineOffset !== undefined) element.style.outlineOffset = styles.outlineOffset;\n  if (styles.objectFit !== undefined) element.style.objectFit = styles.objectFit;\n  if (styles.aspectRatio !== undefined) element.style.aspectRatio = styles.aspectRatio;\n  if (styles.clipPath !== undefined) element.style.clipPath = styles.clipPath;\n  if (styles.overflow !== undefined) element.style.overflow = styles.overflow;\n}\n\n/**\n * Build and apply style properties for a given state with image dimensions\n * This is useful for height-relative clip-path calculations which depend on image height and width\n * @param element - HTML element to apply styles to\n * @param state - Image style state configuration\n * @param imageHeight - Optional image height for height-relative clip-path calculations\n * @param imageWidth - Optional image width for centering height-relative clip-path shapes\n */\nexport function applyStylesToElementWithState(element: HTMLElement, state: ImageStyleState | undefined, imageHeight?: number, imageWidth?: number): void {\n  const styles = buildStyleProperties(state, imageHeight, imageWidth);\n  applyStylesToElement(element, styles);\n}\n\n/**\n * Resolve className to a space-separated string\n */\nexport function resolveClassName(className: string | string[] | undefined): string {\n  if (!className) return '';\n  if (Array.isArray(className)) return className.join(' ');\n  return className;\n}\n\n/**\n * Apply className to element (additive with existing classes)\n */\nexport function applyClassNameToElement(element: HTMLElement, className: string | string[] | undefined): void {\n  const resolved = resolveClassName(className);\n  if (resolved) {\n    resolved.split(' ').forEach(cls => {\n      if (cls.trim()) element.classList.add(cls.trim());\n    });\n  }\n}\n\n/**\n * Remove className from element\n */\nexport function removeClassNameFromElement(element: HTMLElement, className: string | string[] | undefined): void {\n  const resolved = resolveClassName(className);\n  if (resolved) {\n    resolved.split(' ').forEach(cls => {\n      if (cls.trim()) element.classList.remove(cls.trim());\n    });\n  }\n}\n","/**\n * ZoomEngine.ts\n * Manages zoom/focus behavior for image cloud with cross-animation support\n *\n * Public API:\n * - focusImage(imageElement, containerBounds, originalState)\n * - unfocusImage()\n * - getCurrentFocus()\n * - swapFocus(newImageElement, containerBounds, originalState)\n * - isFocused(imageElement)\n * - isAnimating()\n * - getState()\n * - reset()\n */\n\nimport type {\n  FocusInteractionConfig,\n  ContainerBounds,\n  ImageLayout,\n  TransformParams,\n  ImageStylingConfig,\n  AnimationHandle,\n  AnimatingImage\n} from '../config/types';\nimport { ZoomState } from '../config/types';\nimport { AnimationEngine } from './AnimationEngine';\nimport { applyClassNameToElement, removeClassNameFromElement, buildStyleProperties } from '../utils/styleUtils';\n\ninterface FocusData {\n  element: HTMLElement;\n  originalState: ImageLayout;\n  focusTransform: TransformParams;\n  originalZIndex: string;\n  originalWidth: number;\n  originalHeight: number;\n  focusWidth: number;\n  focusHeight: number;\n}\n\n// Z-index constants for layering during animations\nconst Z_INDEX = {\n  DEFAULT: '',\n  UNFOCUSING: 999,\n  FOCUSING: 1000,\n  FOCUSED: 1000\n};\n\nexport class ZoomEngine {\n  private config: FocusInteractionConfig;\n  private animationEngine: AnimationEngine;\n\n  // State machine\n  private state: ZoomState = ZoomState.IDLE;\n  private currentFocus: HTMLElement | null = null;\n  private focusData: FocusData | null = null;\n\n  // Animation tracking for cross-animation\n  private outgoing: AnimatingImage | null = null;\n  private incoming: AnimatingImage | null = null;\n\n  // Generation counter to handle concurrent calls\n  private focusGeneration: number = 0;\n\n  // Styling\n  private styling?: ImageStylingConfig;\n  private focusedClassName: string | string[] | undefined;\n\n  // Callback fired when an unfocus animation fully completes\n  private onUnfocusComplete: ((element: HTMLElement) => void) | null = null;\n\n  constructor(config: FocusInteractionConfig, animationEngine: AnimationEngine, styling?: ImageStylingConfig) {\n    this.config = config;\n    this.animationEngine = animationEngine;\n    this.styling = styling;\n\n    // Store focused class name for on-demand class management\n    this.focusedClassName = styling?.focused?.className;\n  }\n\n  /**\n   * Set callback to be fired when an unfocus animation fully completes.\n   */\n  setOnUnfocusCompleteCallback(callback: ((element: HTMLElement) => void) | null): void {\n    this.onUnfocusComplete = callback;\n  }\n\n  /**\n   * Get current state machine state\n   */\n  getState(): ZoomState {\n    return this.state;\n  }\n\n  /**\n   * Check if any animation is in progress\n   */\n  isAnimating(): boolean {\n    return this.state !== ZoomState.IDLE && this.state !== ZoomState.FOCUSED;\n  }\n\n  /**\n   * Normalize scalePercent value\n   */\n  private normalizeScalePercent(value: number): number {\n    return value > 1 ? value / 100 : value;\n  }\n\n  /**\n   * Calculate target dimensions for focused image\n   * Returns actual pixel dimensions instead of scale factor for sharper rendering\n   */\n  private calculateFocusDimensions(imageWidth: number, imageHeight: number, containerBounds: ContainerBounds): { width: number; height: number } {\n    const normalizedPercent = this.normalizeScalePercent(this.config.scalePercent);\n    const targetHeight = containerBounds.height * normalizedPercent;\n    const aspectRatio = imageWidth / imageHeight;\n\n    let focusHeight = targetHeight;\n    let focusWidth = focusHeight * aspectRatio;\n\n    const maxWidth = containerBounds.width * normalizedPercent;\n    if (focusWidth > maxWidth) {\n      focusWidth = maxWidth;\n      focusHeight = focusWidth / aspectRatio;\n    }\n\n    return { width: focusWidth, height: focusHeight };\n  }\n\n  /**\n   * Calculate the transform needed to center an image (position only, no scale)\n   * Scale is handled by animating actual dimensions for sharper rendering\n   */\n  private calculateFocusTransform(\n    containerBounds: ContainerBounds,\n    originalState: ImageLayout\n  ): TransformParams {\n    const centerX = containerBounds.width / 2;\n    const centerY = containerBounds.height / 2;\n\n    const targetX = centerX - originalState.x;\n    const targetY = centerY - originalState.y;\n\n    return {\n      x: targetX,\n      y: targetY,\n      rotation: 0,\n      scale: 1  // No scale transform - dimensions are animated instead\n    };\n  }\n\n  /**\n   * Build transform string for dimension-based zoom (no scale in transform)\n   */\n  private buildDimensionZoomTransform(params: TransformParams): string {\n    const transforms: string[] = ['translate(-50%, -50%)'];\n\n    if (params.x !== undefined || params.y !== undefined) {\n      const x = params.x ?? 0;\n      const y = params.y ?? 0;\n      transforms.push(`translate(${x}px, ${y}px)`);\n    }\n\n    if (params.rotation !== undefined) {\n      transforms.push(`rotate(${params.rotation}deg)`);\n    }\n\n    // Note: scale is intentionally omitted - we animate width/height instead\n\n    return transforms.join(' ');\n  }\n\n  /**\n   * Create a Web Animation that animates both transform (position) and dimensions\n   * This provides sharper zoom by re-rendering at target size instead of scaling pixels\n   */\n  private animateWithDimensions(\n    element: HTMLElement,\n    fromTransform: TransformParams,\n    toTransform: TransformParams,\n    fromWidth: number,\n    fromHeight: number,\n    toWidth: number,\n    toHeight: number,\n    duration: number\n  ): Animation {\n    const fromTransformStr = this.buildDimensionZoomTransform(fromTransform);\n    const toTransformStr = this.buildDimensionZoomTransform(toTransform);\n\n    // Clear any CSS transitions to avoid conflicts\n    element.style.transition = 'none';\n\n    // Create Web Animation with both transform and dimensions\n    const animation = element.animate(\n      [\n        {\n          transform: fromTransformStr,\n          width: `${fromWidth}px`,\n          height: `${fromHeight}px`\n        },\n        {\n          transform: toTransformStr,\n          width: `${toWidth}px`,\n          height: `${toHeight}px`\n        }\n      ],\n      {\n        duration: duration,\n        easing: 'cubic-bezier(0.4, 0, 0.2, 1)',\n        fill: 'forwards'\n      }\n    );\n\n    return animation;\n  }\n\n  /**\n   * Apply focused styling to an element\n   * Applies all focused styling properties, classes, and z-index\n   */\n  private applyFocusedStyling(element: HTMLElement, zIndex: number): void {\n    element.style.zIndex = String(zIndex);\n    element.classList.add('fbn-ic-focused');\n    applyClassNameToElement(element, this.focusedClassName);\n\n    // Apply all styling properties (opacity, filters, borders, shadows, etc.)\n    // for the focused state\n    if (this.styling?.focused) {\n      const styles = buildStyleProperties(this.styling.focused, element.offsetHeight, element.offsetWidth);\n      // Apply styles directly, skipping clip-path and overflow (updated in startClipPathAnimation)\n      if (styles.borderRadius !== undefined) element.style.borderRadius = styles.borderRadius;\n      if (styles.borderTopLeftRadius !== undefined) element.style.borderTopLeftRadius = styles.borderTopLeftRadius;\n      if (styles.borderTopRightRadius !== undefined) element.style.borderTopRightRadius = styles.borderTopRightRadius;\n      if (styles.borderBottomRightRadius !== undefined) element.style.borderBottomRightRadius = styles.borderBottomRightRadius;\n      if (styles.borderBottomLeftRadius !== undefined) element.style.borderBottomLeftRadius = styles.borderBottomLeftRadius;\n      if (styles.border !== undefined) element.style.border = styles.border;\n      if (styles.borderTop !== undefined) element.style.borderTop = styles.borderTop;\n      if (styles.borderRight !== undefined) element.style.borderRight = styles.borderRight;\n      if (styles.borderBottom !== undefined) element.style.borderBottom = styles.borderBottom;\n      if (styles.borderLeft !== undefined) element.style.borderLeft = styles.borderLeft;\n      if (styles.boxShadow !== undefined) element.style.boxShadow = styles.boxShadow;\n      if (styles.filter !== undefined) element.style.filter = styles.filter;\n      if (styles.opacity !== undefined) element.style.opacity = styles.opacity;\n      if (styles.cursor !== undefined) element.style.cursor = styles.cursor;\n      if (styles.outline !== undefined) element.style.outline = styles.outline;\n      if (styles.outlineOffset !== undefined) element.style.outlineOffset = styles.outlineOffset;\n      if (styles.objectFit !== undefined) element.style.objectFit = styles.objectFit;\n      if (styles.aspectRatio !== undefined) element.style.aspectRatio = styles.aspectRatio;\n    }\n  }\n\n  /**\n   * Remove focused styling from an element\n   * Restores default styling properties, removes classes, and resets z-index\n   */\n  private removeFocusedStyling(element: HTMLElement, originalZIndex: string): void {\n    element.style.zIndex = originalZIndex;\n    element.classList.remove('fbn-ic-focused');\n    removeClassNameFromElement(element, this.focusedClassName);\n\n    // Restore default styling properties (opacity, filters, borders, shadows, etc.)\n    if (this.styling?.default) {\n      const styles = buildStyleProperties(this.styling.default, element.offsetHeight, element.offsetWidth);\n      // Apply styles directly, skipping clip-path and overflow (updated in startClipPathAnimation)\n      if (styles.borderRadius !== undefined) element.style.borderRadius = styles.borderRadius;\n      if (styles.borderTopLeftRadius !== undefined) element.style.borderTopLeftRadius = styles.borderTopLeftRadius;\n      if (styles.borderTopRightRadius !== undefined) element.style.borderTopRightRadius = styles.borderTopRightRadius;\n      if (styles.borderBottomRightRadius !== undefined) element.style.borderBottomRightRadius = styles.borderBottomRightRadius;\n      if (styles.borderBottomLeftRadius !== undefined) element.style.borderBottomLeftRadius = styles.borderBottomLeftRadius;\n      if (styles.border !== undefined) element.style.border = styles.border;\n      if (styles.borderTop !== undefined) element.style.borderTop = styles.borderTop;\n      if (styles.borderRight !== undefined) element.style.borderRight = styles.borderRight;\n      if (styles.borderBottom !== undefined) element.style.borderBottom = styles.borderBottom;\n      if (styles.borderLeft !== undefined) element.style.borderLeft = styles.borderLeft;\n      if (styles.boxShadow !== undefined) element.style.boxShadow = styles.boxShadow;\n      if (styles.filter !== undefined) element.style.filter = styles.filter;\n      if (styles.opacity !== undefined) element.style.opacity = styles.opacity;\n      if (styles.cursor !== undefined) element.style.cursor = styles.cursor;\n      if (styles.outline !== undefined) element.style.outline = styles.outline;\n      if (styles.outlineOffset !== undefined) element.style.outlineOffset = styles.outlineOffset;\n      if (styles.objectFit !== undefined) element.style.objectFit = styles.objectFit;\n      if (styles.aspectRatio !== undefined) element.style.aspectRatio = styles.aspectRatio;\n    }\n  }\n\n  /**\n   * Continuously update clip-path during animation based on current element dimensions\n   * This ensures clip-path changes smoothly as width/height animate\n   */\n  private startClipPathAnimation(element: HTMLElement, handle: AnimationHandle, isToFocused: boolean): void {\n    // Determine which styling config to use\n    // If focused is explicitly defined but has no clipPath, don't fall back to default clipPath\n    let styleConfig: any = isToFocused\n      ? (this.styling?.focused ?? this.styling?.default)\n      : this.styling?.default;\n\n    // If focused config is explicitly set but missing clipPath, explicitly set it to undefined\n    // to prevent inheriting default clipPath\n    if (isToFocused && this.styling?.focused && this.styling.focused.clipPath === undefined) {\n      styleConfig = { ...styleConfig, clipPath: undefined };\n    }\n\n    const updateClipPath = () => {\n      // Use actual animated element dimensions (both width and height are being animated)\n      const currentHeight = element.offsetHeight;\n      const currentWidth = element.offsetWidth;\n\n      // Build style properties with current dimensions to get updated clip-path\n      const styles = buildStyleProperties(styleConfig, currentHeight, currentWidth);\n\n      // Apply clip-path - clear it if not defined\n      if (styles.clipPath !== undefined) {\n        element.style.clipPath = styles.clipPath;\n      } else {\n        // No clip-path defined - clear any inherited clip-path\n        element.style.clipPath = 'unset';\n      }\n      if (styles.overflow !== undefined) {\n        element.style.overflow = styles.overflow;\n      }\n\n      // Continue updating if animation is still running\n      if (handle.animation.playState === 'running') {\n        requestAnimationFrame(updateClipPath);\n      }\n    };\n\n    // Start the update loop\n    requestAnimationFrame(updateClipPath);\n  }\n\n  /**\n   * Start focus animation for an image using dimension-based zoom\n   * Animates actual width/height for sharper rendering instead of transform scale\n   * @param fromTransform - Optional starting transform (for mid-animation reversals)\n   * @param fromDimensions - Optional starting dimensions (for mid-animation reversals)\n   */\n  private startFocusAnimation(\n    element: HTMLElement,\n    containerBounds: ContainerBounds,\n    originalState: ImageLayout,\n    fromTransform?: TransformParams,\n    fromDimensions?: { width: number; height: number }\n  ): AnimatingImage {\n    const originalZIndex = element.style.zIndex || '';\n    const originalWidth = element.offsetWidth;\n    const originalHeight = element.offsetHeight;\n\n    // Calculate target dimensions and position\n    const focusDimensions = this.calculateFocusDimensions(originalWidth, originalHeight, containerBounds);\n    const focusTransform = this.calculateFocusTransform(containerBounds, originalState);\n\n    // Cancel any existing animation\n    this.animationEngine.cancelAllAnimations(element);\n\n    // Get animation duration\n    const duration = this.config.animationDuration ?? 600;\n\n    // Apply focused styling (classes and z-index only)\n    this.applyFocusedStyling(element, Z_INDEX.FOCUSING);\n\n    // Start animation from provided state or original position\n    const startTransform: TransformParams = fromTransform ?? {\n      x: 0,\n      y: 0,\n      rotation: originalState.rotation,\n      scale: 1  // No scale - using dimensions\n    };\n\n    const startWidth = fromDimensions?.width ?? originalWidth;\n    const startHeight = fromDimensions?.height ?? originalHeight;\n\n    // Create dimension-based animation\n    const animation = this.animateWithDimensions(\n      element,\n      startTransform,\n      focusTransform,\n      startWidth,\n      startHeight,\n      focusDimensions.width,\n      focusDimensions.height,\n      duration\n    );\n\n    // Create animation handle\n    const handle: AnimationHandle = {\n      id: `focus-${Date.now()}`,\n      element,\n      animation,\n      fromState: startTransform,\n      toState: focusTransform,\n      startTime: performance.now(),\n      duration\n    };\n\n    // Store focus data for later (including dimensions for unfocus)\n    this.focusData = {\n      element,\n      originalState,\n      focusTransform,\n      originalZIndex,\n      originalWidth,\n      originalHeight,\n      focusWidth: focusDimensions.width,\n      focusHeight: focusDimensions.height\n    };\n\n    // Start continuous clip-path animation to follow dimension changes\n    this.startClipPathAnimation(element, handle, true);\n\n    return {\n      element,\n      originalState,\n      animationHandle: handle,\n      direction: 'in' as const,\n      originalWidth,\n      originalHeight\n    };\n  }\n\n  /**\n   * Start unfocus animation for an image using dimension-based zoom\n   * Animates back to original dimensions for consistent behavior\n   * @param fromDimensions - Optional starting dimensions (for mid-animation reversals)\n   */\n  private startUnfocusAnimation(\n    element: HTMLElement,\n    originalState: ImageLayout,\n    fromTransform?: TransformParams,\n    fromDimensions?: { width: number; height: number }\n  ): AnimatingImage {\n    // Set z-index for unfocusing (below incoming)\n    element.style.zIndex = String(Z_INDEX.UNFOCUSING);\n\n    // Cancel any existing animation\n    this.animationEngine.cancelAllAnimations(element);\n\n    // Get animation duration\n    const duration = this.config.animationDuration ?? 600;\n\n    // Remove focused classes but keep z-index high during animation\n    // (z-index will be reset after animation completes)\n    element.classList.remove('fbn-ic-focused');\n    removeClassNameFromElement(element, this.focusedClassName);\n\n    // Start from current focused state (or provided state for interrupted animations)\n    const startTransform = fromTransform ?? this.focusData?.focusTransform ?? { x: 0, y: 0, rotation: 0, scale: 1 };\n    const startWidth = fromDimensions?.width ?? this.focusData?.focusWidth ?? element.offsetWidth;\n    const startHeight = fromDimensions?.height ?? this.focusData?.focusHeight ?? element.offsetHeight;\n\n    // Target is original position and dimensions\n    const toState: TransformParams = {\n      x: 0,\n      y: 0,\n      rotation: originalState.rotation,\n      scale: 1  // No scale - using dimensions\n    };\n\n    const targetWidth = this.focusData?.originalWidth ?? element.offsetWidth;\n    const targetHeight = this.focusData?.originalHeight ?? element.offsetHeight;\n\n    // Create dimension-based animation\n    const animation = this.animateWithDimensions(\n      element,\n      startTransform,\n      toState,\n      startWidth,\n      startHeight,\n      targetWidth,\n      targetHeight,\n      duration\n    );\n\n    // Create animation handle\n    const handle: AnimationHandle = {\n      id: `unfocus-${Date.now()}`,\n      element,\n      animation,\n      fromState: startTransform,\n      toState: toState,\n      startTime: performance.now(),\n      duration\n    };\n\n    // Start continuous clip-path animation to follow dimension changes back to default\n    this.startClipPathAnimation(element, handle, false);\n\n    return {\n      element,\n      originalState,\n      animationHandle: handle,\n      direction: 'out' as const,\n      originalWidth: targetWidth,\n      originalHeight: targetHeight\n    };\n  }\n\n  /**\n   * Capture the current visual state of an element mid-animation, BEFORE cancelling.\n   *\n   * The computed matrix.e/f include the -50%/-50% centering offset resolved to pixels.\n   * buildDimensionZoomTransform prepends its own translate(-50%,-50%), so passing raw\n   * matrix.e/f doubles the centering and produces the wrong starting position.\n   *\n   * This method extracts the PURE positional offset (pureX = matrix.e + 0.5*midWidth)\n   * and commits width/height/transform to inline styles before the animation is cancelled,\n   * preventing any visual snap.\n   *\n   * Must be called while the animation is still running (offsetWidth reflects animated size).\n   * Caller is responsible for calling animationEngine.cancelAllAnimations() afterwards.\n   */\n  private captureMidAnimationState(element: HTMLElement): {\n    transform: TransformParams;\n    dimensions: { width: number; height: number };\n  } {\n    const computed = getComputedStyle(element);\n    const matrix = new DOMMatrix(computed.transform);\n    const midWidth = element.offsetWidth;\n    const midHeight = element.offsetHeight;\n\n    // Remove the -50%/-50% centering that is baked into matrix.e/f\n    const pureX = matrix.e + midWidth * 0.5;\n    const pureY = matrix.f + midHeight * 0.5;\n    const rotation = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);\n\n    // Commit current visual state to inline styles so cancelling the animation\n    // does not cause the element to snap to a different size/position\n    element.style.width = `${midWidth}px`;\n    element.style.height = `${midHeight}px`;\n    element.style.transform = `translate(-50%, -50%) translate(${pureX}px, ${pureY}px) rotate(${rotation}deg)`;\n    element.style.transition = 'none';\n\n    return {\n      transform: { x: pureX, y: pureY, rotation, scale: 1 },\n      dimensions: { width: midWidth, height: midHeight }\n    };\n  }\n\n  /**\n   * Handle animation completion\n   */\n  private async waitForAnimation(handle: AnimationHandle): Promise<void> {\n    try {\n      await handle.animation.finished;\n    } catch {\n      // Animation was cancelled - this is expected during interruption\n    }\n  }\n\n  /**\n   * Reset an element instantly to its original position and dimensions (no animation)\n   */\n  private resetElementInstantly(\n    element: HTMLElement,\n    originalState: ImageLayout,\n    originalZIndex: string,\n    originalWidth?: number,\n    originalHeight?: number\n  ): void {\n    // Cancel any active animation (including completed animations with fill: 'forwards')\n    this.animationEngine.cancelAllAnimations(element);\n\n    // Build transform string for original position (no scale - using dimensions)\n    const transforms = ['translate(-50%, -50%)'];\n    transforms.push('translate(0px, 0px)');\n    transforms.push(`rotate(${originalState.rotation}deg)`);\n    // No scale in transform - dimensions handle sizing\n\n    element.style.transition = 'none';\n    element.style.transform = transforms.join(' ');\n\n    // Restore original dimensions if provided\n    if (originalWidth !== undefined && originalHeight !== undefined) {\n      element.style.width = `${originalWidth}px`;\n      element.style.height = `${originalHeight}px`;\n    }\n\n    // Remove focused styling (clip-path will be updated when animating)\n    this.removeFocusedStyling(element, originalZIndex);\n  }\n\n  /**\n   * Focus (zoom) an image to center of container\n   * Implements cross-animation when swapping focus\n   */\n  async focusImage(\n    imageElement: HTMLElement,\n    containerBounds: ContainerBounds,\n    originalState: ImageLayout\n  ): Promise<void> {\n    // Same image clicked while already focused - unfocus it\n    if (this.currentFocus === imageElement && this.state === ZoomState.FOCUSED) {\n      return this.unfocusImage();\n    }\n\n    // Same image clicked while it's animating in - reverse to unfocus\n    if (this.incoming?.element === imageElement && this.state === ZoomState.FOCUSING) {\n      // Capture mid-animation state BEFORE cancelling (offsetWidth reflects animated size;\n      // pure positional offset strips the -50% centering already baked into matrix.e/f)\n      const { transform: fromTransform, dimensions: fromDimensions } =\n        this.captureMidAnimationState(imageElement);\n      this.animationEngine.cancelAllAnimations(imageElement);\n\n      this.outgoing = this.startUnfocusAnimation(\n        imageElement,\n        this.incoming.originalState,\n        fromTransform,\n        fromDimensions\n      );\n      this.incoming = null;\n      this.state = ZoomState.UNFOCUSING;\n\n      await this.waitForAnimation(this.outgoing.animationHandle);\n      this.removeFocusedStyling(this.outgoing.element, this.focusData?.originalZIndex || '');\n      this.outgoing = null;\n      this.currentFocus = null;\n      this.focusData = null;\n      this.state = ZoomState.IDLE;\n      return;\n    }\n\n    // Increment generation to invalidate any previous in-flight calls\n    const myGeneration = ++this.focusGeneration;\n\n    switch (this.state) {\n      case ZoomState.IDLE:\n        // Simple focus - no current focus\n        this.state = ZoomState.FOCUSING;\n        this.incoming = this.startFocusAnimation(imageElement, containerBounds, originalState);\n\n        await this.waitForAnimation(this.incoming.animationHandle);\n\n        // Check if we're still the active generation\n        if (this.focusGeneration !== myGeneration) return;\n\n        this.currentFocus = imageElement;\n        this.incoming = null;\n        this.state = ZoomState.FOCUSED;\n        break;\n\n      case ZoomState.FOCUSED:\n        // Cross-animation: unfocus current while focusing new\n        this.state = ZoomState.CROSS_ANIMATING;\n\n        // Start outgoing animation for currently focused image\n        if (this.currentFocus && this.focusData) {\n          this.outgoing = this.startUnfocusAnimation(\n            this.currentFocus,\n            this.focusData.originalState\n          );\n        }\n\n        // Start incoming animation for new image\n        this.incoming = this.startFocusAnimation(imageElement, containerBounds, originalState);\n\n        // Wait for both animations\n        await Promise.all([\n          this.outgoing ? this.waitForAnimation(this.outgoing.animationHandle) : Promise.resolve(),\n          this.waitForAnimation(this.incoming.animationHandle)\n        ]);\n\n        // Check if we're still the active generation\n        if (this.focusGeneration !== myGeneration) {\n          return;\n        }\n\n        // Cleanup outgoing\n        if (this.outgoing) {\n          const completedOutgoing = this.outgoing.element;\n          this.removeFocusedStyling(completedOutgoing, this.outgoing.originalState.zIndex?.toString() || '');\n          this.outgoing = null;\n          this.onUnfocusComplete?.(completedOutgoing);\n        }\n\n        this.currentFocus = imageElement;\n        this.incoming = null;\n        this.state = ZoomState.FOCUSED;\n        break;\n\n      case ZoomState.FOCUSING:\n        // New image clicked while another is focusing\n        // Cancel the incoming and start new focus\n        if (this.incoming) {\n          this.animationEngine.cancelAnimation(this.incoming.animationHandle, false);\n          this.resetElementInstantly(\n            this.incoming.element,\n            this.incoming.originalState,\n            this.focusData?.originalZIndex || '',\n            this.focusData?.originalWidth,\n            this.focusData?.originalHeight\n          );\n          this.incoming = null;\n        }\n\n        // Start focusing the new image\n        this.incoming = this.startFocusAnimation(imageElement, containerBounds, originalState);\n\n        await this.waitForAnimation(this.incoming.animationHandle);\n\n        // Check if we're still the active generation\n        if (this.focusGeneration !== myGeneration) return;\n\n        this.currentFocus = imageElement;\n        this.incoming = null;\n        this.state = ZoomState.FOCUSED;\n        break;\n\n      case ZoomState.UNFOCUSING:\n        // New image clicked while current is unfocusing\n        // Let outgoing continue, start cross-animation\n        this.state = ZoomState.CROSS_ANIMATING;\n        this.incoming = this.startFocusAnimation(imageElement, containerBounds, originalState);\n\n        await Promise.all([\n          this.outgoing ? this.waitForAnimation(this.outgoing.animationHandle) : Promise.resolve(),\n          this.waitForAnimation(this.incoming.animationHandle)\n        ]);\n\n        // Check if we're still the active generation\n        if (this.focusGeneration !== myGeneration) return;\n\n        if (this.outgoing) {\n          const completedOutgoing = this.outgoing.element;\n          this.removeFocusedStyling(completedOutgoing, this.outgoing.originalState.zIndex?.toString() || '');\n          this.outgoing = null;\n          this.onUnfocusComplete?.(completedOutgoing);\n        }\n\n        this.currentFocus = imageElement;\n        this.incoming = null;\n        this.state = ZoomState.FOCUSED;\n        break;\n\n      case ZoomState.CROSS_ANIMATING:\n        // Handle click during cross-animation\n\n        // If clicking the same image that's animating in, ignore (it's already targeting focus)\n        if (this.incoming?.element === imageElement) {\n          return;\n        }\n\n        // If clicking the same image that's animating out, let it become the new focus target\n        // (reverse direction - it should animate back to focused)\n        if (this.outgoing?.element === imageElement) {\n          // Capture mid-animation state for outgoing before cancelling\n          const { transform: fromTransform, dimensions: fromDimensions } =\n            this.captureMidAnimationState(imageElement);\n          this.animationEngine.cancelAllAnimations(imageElement);\n\n          // Redirect current incoming to become outgoing\n          if (this.incoming) {\n            const { transform: incomingFrom, dimensions: incomingFromDimensions } =\n              this.captureMidAnimationState(this.incoming.element);\n            this.animationEngine.cancelAllAnimations(this.incoming.element);\n            this.outgoing = this.startUnfocusAnimation(\n              this.incoming.element,\n              this.incoming.originalState,\n              incomingFrom,\n              incomingFromDimensions\n            );\n          } else {\n            this.outgoing = null;\n          }\n\n          // Start new incoming for the clicked (formerly outgoing) image\n          // Use fromTransform and fromDimensions to continue from current state\n          this.incoming = this.startFocusAnimation(imageElement, containerBounds, originalState, fromTransform, fromDimensions);\n\n          await Promise.all([\n            this.outgoing ? this.waitForAnimation(this.outgoing.animationHandle) : Promise.resolve(),\n            this.waitForAnimation(this.incoming.animationHandle)\n          ]);\n\n          if (this.focusGeneration !== myGeneration) return;\n\n          if (this.outgoing) {\n            const completedOutgoing = this.outgoing.element;\n            this.removeFocusedStyling(completedOutgoing, this.outgoing.originalState.zIndex?.toString() || '');\n            this.outgoing = null;\n            this.onUnfocusComplete?.(completedOutgoing);\n          }\n\n          this.currentFocus = imageElement;\n          this.incoming = null;\n          this.state = ZoomState.FOCUSED;\n          return;\n        }\n\n        // Third different image clicked during cross-animation\n        // 1. Reset outgoing instantly\n        // 2. Redirect incoming to become outgoing\n        // 3. Start new incoming\n\n        // Reset outgoing instantly\n        if (this.outgoing) {\n          this.animationEngine.cancelAnimation(this.outgoing.animationHandle, false);\n          this.resetElementInstantly(\n            this.outgoing.element,\n            this.outgoing.originalState,\n            this.outgoing.originalState.zIndex?.toString() || '',\n            this.outgoing.originalWidth,\n            this.outgoing.originalHeight\n          );\n          this.outgoing = null;\n        }\n\n        // Redirect incoming to outgoing (animate from current position back to original)\n        if (this.incoming) {\n          const { transform: fromTransform, dimensions: fromDimensions } =\n            this.captureMidAnimationState(this.incoming.element);\n          this.animationEngine.cancelAllAnimations(this.incoming.element);\n\n          this.outgoing = this.startUnfocusAnimation(\n            this.incoming.element,\n            this.incoming.originalState,\n            fromTransform,\n            fromDimensions\n          );\n        }\n\n        // Start new incoming\n        this.incoming = this.startFocusAnimation(imageElement, containerBounds, originalState);\n\n        // Wait for both\n        await Promise.all([\n          this.outgoing ? this.waitForAnimation(this.outgoing.animationHandle) : Promise.resolve(),\n          this.waitForAnimation(this.incoming.animationHandle)\n        ]);\n\n        // Check if we're still the active generation\n        if (this.focusGeneration !== myGeneration) return;\n\n        if (this.outgoing) {\n          const completedOutgoing = this.outgoing.element;\n          this.removeFocusedStyling(completedOutgoing, this.outgoing.originalState.zIndex?.toString() || '');\n          this.outgoing = null;\n          this.onUnfocusComplete?.(completedOutgoing);\n        }\n\n        this.currentFocus = imageElement;\n        this.incoming = null;\n        this.state = ZoomState.FOCUSED;\n        break;\n    }\n  }\n\n  /**\n   * Unfocus current image, returning it to original position\n   */\n  async unfocusImage(): Promise<void> {\n    // Already animating out - ignore duplicate requests (e.g. ESC pressed twice)\n    if (this.state === ZoomState.UNFOCUSING) {\n      return;\n    }\n\n    // Increment generation to invalidate any previous in-flight calls\n    const myGeneration = ++this.focusGeneration;\n\n    if (!this.currentFocus || !this.focusData) {\n      // Handle case where we're in FOCUSING state\n      if (this.incoming && this.state === ZoomState.FOCUSING) {\n        const { transform: fromTransform, dimensions: fromDimensions } =\n          this.captureMidAnimationState(this.incoming.element);\n        this.animationEngine.cancelAllAnimations(this.incoming.element);\n\n        this.outgoing = this.startUnfocusAnimation(\n          this.incoming.element,\n          this.incoming.originalState,\n          fromTransform,\n          fromDimensions\n        );\n        this.incoming = null;\n        this.state = ZoomState.UNFOCUSING;\n\n        await this.waitForAnimation(this.outgoing.animationHandle);\n\n        if (this.focusGeneration !== myGeneration) return;\n\n        const completedFocusing = this.outgoing.element;\n        this.removeFocusedStyling(completedFocusing, this.focusData?.originalZIndex || '');\n        this.outgoing = null;\n        this.focusData = null;\n        this.state = ZoomState.IDLE;\n        this.onUnfocusComplete?.(completedFocusing);\n      }\n      return;\n    }\n\n    // Handle cross-animation cancellation (ESC during cross-animation)\n    if (this.state === ZoomState.CROSS_ANIMATING) {\n      // Cancel incoming and animate it back\n      if (this.incoming) {\n        const { transform: fromTransform, dimensions: fromDimensions } =\n          this.captureMidAnimationState(this.incoming.element);\n        this.animationEngine.cancelAllAnimations(this.incoming.element);\n\n        // Start unfocus for incoming from its current position\n        const incomingUnfocus = this.startUnfocusAnimation(\n          this.incoming.element,\n          this.incoming.originalState,\n          fromTransform,\n          fromDimensions\n        );\n\n        // Wait for both outgoing and the redirected incoming\n        await Promise.all([\n          this.outgoing ? this.waitForAnimation(this.outgoing.animationHandle) : Promise.resolve(),\n          this.waitForAnimation(incomingUnfocus.animationHandle)\n        ]);\n\n        if (this.focusGeneration !== myGeneration) return;\n\n        // Cleanup\n        let outgoingElement: HTMLElement | null = null;\n        if (this.outgoing) {\n          outgoingElement = this.outgoing.element;\n          this.removeFocusedStyling(outgoingElement, this.outgoing.originalState.zIndex?.toString() || '');\n        }\n        const incomingUnfocusElement = incomingUnfocus.element;\n        this.removeFocusedStyling(incomingUnfocusElement, this.incoming.originalState.zIndex?.toString() || '');\n\n        this.outgoing = null;\n        this.incoming = null;\n        this.currentFocus = null;\n        this.focusData = null;\n        this.state = ZoomState.IDLE;\n\n        if (outgoingElement) this.onUnfocusComplete?.(outgoingElement);\n        this.onUnfocusComplete?.(incomingUnfocusElement);\n        return;\n      }\n    }\n\n    // Normal unfocus from FOCUSED state\n    this.state = ZoomState.UNFOCUSING;\n    const element = this.currentFocus;\n    const originalState = this.focusData.originalState;\n    const originalZIndex = this.focusData.originalZIndex;\n\n    this.outgoing = this.startUnfocusAnimation(element, originalState);\n\n    await this.waitForAnimation(this.outgoing.animationHandle);\n\n    if (this.focusGeneration !== myGeneration) return;\n\n    this.removeFocusedStyling(element, originalZIndex);\n    this.outgoing = null;\n    this.currentFocus = null;\n    this.focusData = null;\n    this.state = ZoomState.IDLE;\n    this.onUnfocusComplete?.(element);\n  }\n\n  /**\n   * Swap focus from current image to a new one (alias for focusImage with cross-animation)\n   */\n  async swapFocus(\n    newImageElement: HTMLElement,\n    containerBounds: ContainerBounds,\n    originalState: ImageLayout\n  ): Promise<void> {\n    return this.focusImage(newImageElement, containerBounds, originalState);\n  }\n\n  /**\n   * Get currently focused image element\n   */\n  getCurrentFocus(): HTMLElement | null {\n    return this.currentFocus;\n  }\n\n  /**\n   * Check if an image is currently focused (stable state)\n   */\n  isFocused(imageElement: HTMLElement): boolean {\n    return this.currentFocus === imageElement && this.state === ZoomState.FOCUSED;\n  }\n\n  /**\n   * Check if an image is the target of current focus animation\n   */\n  isTargetingFocus(imageElement: HTMLElement): boolean {\n    return this.incoming?.element === imageElement;\n  }\n\n  /**\n   * Check if an image is involved in any focus/animation state\n   * Returns true if the image is focused, animating in, or animating out\n   * Useful for hover state management - don't apply hover to animating images\n   */\n  isInvolved(imageElement: HTMLElement): boolean {\n    return (\n      this.currentFocus === imageElement ||\n      this.incoming?.element === imageElement ||\n      this.outgoing?.element === imageElement\n    );\n  }\n\n  /**\n   * Apply a temporary horizontal drag offset to the focused image\n   * Used during swipe gestures for visual feedback\n   */\n  setDragOffset(offset: number): void {\n    if (!this.currentFocus || !this.focusData || this.state !== ZoomState.FOCUSED) return;\n\n    const element = this.currentFocus;\n    const focusTransform = this.focusData.focusTransform;\n\n    // Build transform with additional horizontal offset\n    const transforms: string[] = ['translate(-50%, -50%)'];\n    const x = (focusTransform.x ?? 0) + offset;\n    const y = focusTransform.y ?? 0;\n    transforms.push(`translate(${x}px, ${y}px)`);\n    if (focusTransform.rotation !== undefined) {\n      transforms.push(`rotate(${focusTransform.rotation}deg)`);\n    }\n\n    element.style.transition = 'none';\n    element.style.transform = transforms.join(' ');\n  }\n\n  /**\n   * Clear the drag offset, optionally animating back to center\n   * @param animate - If true, animate back to center; if false, snap instantly\n   * @param duration - Animation duration in ms (default 150)\n   */\n  clearDragOffset(animate: boolean, duration: number = 150): void {\n    if (!this.currentFocus || !this.focusData || this.state !== ZoomState.FOCUSED) return;\n\n    const element = this.currentFocus;\n    const focusTransform = this.focusData.focusTransform;\n\n    // Build the centered transform (no offset)\n    const transforms: string[] = ['translate(-50%, -50%)'];\n    const x = focusTransform.x ?? 0;\n    const y = focusTransform.y ?? 0;\n    transforms.push(`translate(${x}px, ${y}px)`);\n    if (focusTransform.rotation !== undefined) {\n      transforms.push(`rotate(${focusTransform.rotation}deg)`);\n    }\n    const centeredTransform = transforms.join(' ');\n\n    if (animate) {\n      element.style.transition = `transform ${duration}ms ease-out`;\n      element.style.transform = centeredTransform;\n      // Clear transition after animation completes\n      setTimeout(() => {\n        if (this.currentFocus === element) {\n          element.style.transition = 'none';\n        }\n      }, duration);\n    } else {\n      element.style.transition = 'none';\n      element.style.transform = centeredTransform;\n    }\n  }\n\n  /**\n   * Reset zoom state (cancels all animations)\n   */\n  reset(): void {\n    // Cancel any active animations\n    if (this.outgoing) {\n      this.animationEngine.cancelAnimation(this.outgoing.animationHandle, false);\n      this.resetElementInstantly(\n        this.outgoing.element,\n        this.outgoing.originalState,\n        this.outgoing.originalState.zIndex?.toString() || '',\n        this.outgoing.originalWidth,\n        this.outgoing.originalHeight\n      );\n    }\n\n    if (this.incoming) {\n      this.animationEngine.cancelAnimation(this.incoming.animationHandle, false);\n      this.resetElementInstantly(\n        this.incoming.element,\n        this.incoming.originalState,\n        this.focusData?.originalZIndex || '',\n        this.focusData?.originalWidth,\n        this.focusData?.originalHeight\n      );\n    }\n\n    if (this.currentFocus && this.focusData) {\n      this.resetElementInstantly(\n        this.currentFocus,\n        this.focusData.originalState,\n        this.focusData.originalZIndex,\n        this.focusData.originalWidth,\n        this.focusData.originalHeight\n      );\n    }\n\n    this.state = ZoomState.IDLE;\n    this.currentFocus = null;\n    this.focusData = null;\n    this.outgoing = null;\n    this.incoming = null;\n  }\n}\n","/**\n * SwipeEngine.ts\n * Handles touch swipe gestures for navigating between focused images\n *\n * Public API:\n * - enable() - Start listening for touch events\n * - disable() - Stop listening (when no image focused)\n * - destroy() - Clean up all event listeners\n * - setDragCallback(callback) - Set callback for drag offset updates\n */\n\n// Constants for swipe detection\nconst SWIPE_THRESHOLD_PX = 50;\nconst SWIPE_VELOCITY_THRESHOLD = 0.5; // px/ms\nconst SWIPE_MIN_DISTANCE_FOR_VELOCITY = 20;\nconst DRAG_DAMPING = 0.3;\nconst SNAP_BACK_DURATION_MS = 150;\nconst HORIZONTAL_ANGLE_THRESHOLD_DEG = 30;\n\ninterface SwipeCallbacks {\n  onNext: () => void;\n  onPrev: () => void;\n  onDragOffset: (offset: number) => void;\n  onDragEnd: (navigated: boolean) => void;\n}\n\ninterface TouchState {\n  startX: number;\n  startY: number;\n  startTime: number;\n  currentX: number;\n  isDragging: boolean;\n  isHorizontalSwipe: boolean | null; // null = not determined yet\n}\n\nexport class SwipeEngine {\n  private container: HTMLElement;\n  private callbacks: SwipeCallbacks;\n  private enabled: boolean = false;\n  private touchState: TouchState | null = null;\n\n  // Track recent touch activity to prevent click-outside from unfocusing\n  private recentTouchTimestamp: number = 0;\n  private static readonly TOUCH_CLICK_DELAY = 300; // ms to ignore clicks after touch\n\n  // Bound event handlers for proper cleanup\n  private boundTouchStart: (e: TouchEvent) => void;\n  private boundTouchMove: (e: TouchEvent) => void;\n  private boundTouchEnd: (e: TouchEvent) => void;\n  private boundTouchCancel: (e: TouchEvent) => void;\n\n  constructor(container: HTMLElement, callbacks: SwipeCallbacks) {\n    this.container = container;\n    this.callbacks = callbacks;\n\n    // Bind handlers\n    this.boundTouchStart = this.handleTouchStart.bind(this);\n    this.boundTouchMove = this.handleTouchMove.bind(this);\n    this.boundTouchEnd = this.handleTouchEnd.bind(this);\n    this.boundTouchCancel = this.handleTouchCancel.bind(this);\n  }\n\n  /**\n   * Start listening for touch events\n   */\n  enable(): void {\n    if (this.enabled) return;\n    this.enabled = true;\n\n    // Set touch-action to allow vertical scroll but let JS handle horizontal\n    this.container.style.touchAction = 'pan-y';\n\n    this.container.addEventListener('touchstart', this.boundTouchStart, { passive: false });\n    this.container.addEventListener('touchmove', this.boundTouchMove, { passive: false });\n    this.container.addEventListener('touchend', this.boundTouchEnd, { passive: true });\n    this.container.addEventListener('touchcancel', this.boundTouchCancel, { passive: true });\n  }\n\n  /**\n   * Stop listening for touch events\n   */\n  disable(): void {\n    if (!this.enabled) return;\n    this.enabled = false;\n\n    // Restore default touch-action\n    this.container.style.touchAction = '';\n\n    this.container.removeEventListener('touchstart', this.boundTouchStart);\n    this.container.removeEventListener('touchmove', this.boundTouchMove);\n    this.container.removeEventListener('touchend', this.boundTouchEnd);\n    this.container.removeEventListener('touchcancel', this.boundTouchCancel);\n\n    // Reset any in-progress drag\n    if (this.touchState?.isDragging) {\n      this.callbacks.onDragEnd(false);\n    }\n    this.touchState = null;\n  }\n\n  /**\n   * Clean up all event listeners\n   */\n  destroy(): void {\n    this.disable();\n  }\n\n  /**\n   * Check if a touch interaction happened recently\n   * Used to prevent click-outside from unfocusing immediately after touch\n   */\n  hadRecentTouch(): boolean {\n    return Date.now() - this.recentTouchTimestamp < SwipeEngine.TOUCH_CLICK_DELAY;\n  }\n\n  private handleTouchStart(e: TouchEvent): void {\n    if (e.touches.length !== 1) return;\n\n    // Mark recent touch to prevent click-outside unfocus\n    this.recentTouchTimestamp = Date.now();\n\n    const touch = e.touches[0];\n    this.touchState = {\n      startX: touch.clientX,\n      startY: touch.clientY,\n      startTime: performance.now(),\n      currentX: touch.clientX,\n      isDragging: false,\n      isHorizontalSwipe: null\n    };\n  }\n\n  private handleTouchMove(e: TouchEvent): void {\n    if (!this.touchState || e.touches.length !== 1) return;\n\n    const touch = e.touches[0];\n    const deltaX = touch.clientX - this.touchState.startX;\n    const deltaY = touch.clientY - this.touchState.startY;\n\n    // Determine swipe direction on first significant movement\n    if (this.touchState.isHorizontalSwipe === null) {\n      const totalDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);\n      if (totalDistance > 10) {\n        // Calculate angle from horizontal\n        const angleRad = Math.atan2(Math.abs(deltaY), Math.abs(deltaX));\n        const angleDeg = angleRad * (180 / Math.PI);\n        this.touchState.isHorizontalSwipe = angleDeg <= HORIZONTAL_ANGLE_THRESHOLD_DEG;\n      }\n    }\n\n    // If determined to be vertical, don't capture\n    if (this.touchState.isHorizontalSwipe === false) {\n      return;\n    }\n\n    // If horizontal swipe, prevent default and track drag\n    if (this.touchState.isHorizontalSwipe === true) {\n      e.preventDefault();\n      this.touchState.isDragging = true;\n      this.touchState.currentX = touch.clientX;\n\n      // Apply damped offset to focused image\n      const dampedOffset = deltaX * DRAG_DAMPING;\n      this.callbacks.onDragOffset(dampedOffset);\n    }\n  }\n\n  private handleTouchEnd(_e: TouchEvent): void {\n    if (!this.touchState) return;\n\n    // Update timestamp to prevent click-outside unfocus\n    this.recentTouchTimestamp = Date.now();\n\n    const deltaX = this.touchState.currentX - this.touchState.startX;\n    const deltaTime = performance.now() - this.touchState.startTime;\n    const velocity = Math.abs(deltaX) / deltaTime;\n    const absDistance = Math.abs(deltaX);\n\n    let navigated = false;\n\n    // Only process if it was a horizontal swipe\n    if (this.touchState.isHorizontalSwipe === true && this.touchState.isDragging) {\n      // Check if swipe threshold met (distance or velocity)\n      const thresholdMet =\n        absDistance >= SWIPE_THRESHOLD_PX ||\n        (velocity >= SWIPE_VELOCITY_THRESHOLD && absDistance >= SWIPE_MIN_DISTANCE_FOR_VELOCITY);\n\n      if (thresholdMet) {\n        navigated = true;\n        if (deltaX < 0) {\n          // Swipe left -> next image\n          this.callbacks.onNext();\n        } else {\n          // Swipe right -> previous image\n          this.callbacks.onPrev();\n        }\n      }\n    }\n\n    // Notify drag end (handles snap-back if not navigated)\n    if (this.touchState.isDragging) {\n      this.callbacks.onDragEnd(navigated);\n    }\n\n    this.touchState = null;\n  }\n\n  private handleTouchCancel(_e: TouchEvent): void {\n    if (this.touchState?.isDragging) {\n      this.callbacks.onDragEnd(false);\n    }\n    this.touchState = null;\n  }\n}\n\nexport { SNAP_BACK_DURATION_MS };\n","/**\n * GoogleDriveLoader.ts\n * Loads images from a public Google Drive folder\n *\n * Public API:\n * - prepare(filter) - Async discovery of images\n * - imagesLength() - Get count of discovered images\n * - imageURLs() - Get ordered list of image URLs\n * - isPrepared() - Check if loader has been prepared\n *\n * Helper methods (for advanced usage):\n * - extractFolderId(folderUrl)\n * - manualImageUrls(imageIds)\n */\n\nimport type { ImageLoader, IImageFilter, GoogleDriveResponse, GoogleDriveLoaderInnerConfig, GoogleDriveSource } from '../config/types';\n\nexport class GoogleDriveLoader implements ImageLoader {\n  private apiKey: string;\n  private apiEndpoint: string;\n  private debugLogging: boolean;\n  private sources: GoogleDriveSource[];\n\n  // State for new interface\n  private _prepared: boolean = false;\n  private _discoveredUrls: string[] = [];\n\n  constructor(config: GoogleDriveLoaderInnerConfig) {\n    this.apiKey = config.apiKey ?? '';\n    this.apiEndpoint = config.apiEndpoint ?? 'https://www.googleapis.com/drive/v3/files';\n    this.debugLogging = config.debugLogging ?? false;\n    this.sources = config.sources ?? [];\n\n    // Validate that we have sources configured\n    if (!this.sources || this.sources.length === 0) {\n      throw new Error('GoogleDriveLoader requires at least one source to be configured');\n    }\n  }\n\n  /**\n   * Prepare the loader by discovering all images from configured sources\n   * @param filter - Filter to apply to discovered images\n   */\n  async prepare(filter: IImageFilter): Promise<void> {\n    this._discoveredUrls = [];\n\n    for (const source of this.sources) {\n      if ('folders' in source) {\n        for (const folderUrl of source.folders) {\n          const recursive = source.recursive !== undefined ? source.recursive : true;\n          const urls = await this.loadFromFolder(folderUrl, filter, recursive);\n          this._discoveredUrls.push(...urls);\n        }\n      } else if ('files' in source) {\n        const urls = await this.loadFiles(source.files, filter);\n        this._discoveredUrls.push(...urls);\n      }\n    }\n\n    this._prepared = true;\n  }\n\n  /**\n   * Get the number of discovered images\n   * @throws Error if called before prepare()\n   */\n  imagesLength(): number {\n    if (!this._prepared) {\n      throw new Error('GoogleDriveLoader.imagesLength() called before prepare()');\n    }\n    return this._discoveredUrls.length;\n  }\n\n  /**\n   * Get the ordered list of image URLs\n   * @throws Error if called before prepare()\n   */\n  imageURLs(): string[] {\n    if (!this._prepared) {\n      throw new Error('GoogleDriveLoader.imageURLs() called before prepare()');\n    }\n    return [...this._discoveredUrls];\n  }\n\n  /**\n   * Check if the loader has been prepared\n   */\n  isPrepared(): boolean {\n    return this._prepared;\n  }\n\n  /**\n   * Extract folder ID from various Google Drive URL formats\n   * @param folderUrl - Google Drive folder URL\n   * @returns Folder ID or null if invalid\n   */\n  extractFolderId(folderUrl: string): string | null {\n    // Handle various URL formats:\n    // https://drive.google.com/drive/folders/FOLDER_ID\n    // https://drive.google.com/drive/folders/FOLDER_ID?usp=sharing\n\n    const patterns = [\n      /\\/folders\\/([a-zA-Z0-9_-]+)/,  // Standard format\n      /id=([a-zA-Z0-9_-]+)/  // Alternative format\n    ];\n\n    for (const pattern of patterns) {\n      const match = folderUrl.match(pattern);\n      if (match && match[1]) {\n        return match[1];\n      }\n    }\n\n    return null;\n  }\n\n  /**\n   * Load images from a Google Drive folder\n   * @param folderUrl - Google Drive folder URL\n   * @param filter - Filter to apply to discovered images\n   * @param recursive - Whether to include images from subfolders\n   * @returns Promise resolving to array of image URLs\n   */\n  private async loadFromFolder(folderUrl: string, filter: IImageFilter, recursive: boolean = true): Promise<string[]> {\n    const folderId = this.extractFolderId(folderUrl);\n\n    if (!folderId) {\n      throw new Error('Invalid Google Drive folder URL. Please check the URL format.');\n    }\n\n    // If no API key is configured, use direct link method\n    if (!this.apiKey || this.apiKey === 'YOUR_API_KEY_HERE') {\n      return this.loadImagesDirectly(folderId, filter);\n    }\n\n    try {\n      if (recursive) {\n        return await this.loadImagesRecursively(folderId, filter);\n      } else {\n        return await this.loadImagesFromSingleFolder(folderId, filter);\n      }\n    } catch (error) {\n      console.error('Error loading from Google Drive API:', error);\n      // Fallback to direct link method\n      return this.loadImagesDirectly(folderId, filter);\n    }\n  }\n\n  /**\n   * Load images from a single folder (non-recursive)\n   * @param folderId - Google Drive folder ID\n   * @param filter - Filter to apply to discovered images\n   * @returns Promise resolving to array of image URLs\n   */\n  private async loadImagesFromSingleFolder(folderId: string, filter: IImageFilter): Promise<string[]> {\n    const imageUrls: string[] = [];\n\n    // Query for all files in this folder\n    const query = `'${folderId}' in parents and trashed=false`;\n    const fields = 'files(id,name,mimeType,thumbnailLink)';\n    const url = `${this.apiEndpoint}?q=${encodeURIComponent(query)}&fields=${fields}&key=${this.apiKey}`;\n\n    const response = await fetch(url);\n\n    if (!response.ok) {\n      throw new Error(`API request failed: ${response.status} ${response.statusText}`);\n    }\n\n    const data: GoogleDriveResponse = await response.json();\n\n    // Filter for valid image files only using the provided filter\n    const validFiles = data.files.filter(file =>\n      file.mimeType.startsWith('image/') && filter.isAllowed(file.name)\n    );\n\n    this.log(`Found ${validFiles.length} images in folder ${folderId} (non-recursive)`);\n\n    // Add image URLs\n    validFiles.forEach(file => {\n      imageUrls.push(`https://lh3.googleusercontent.com/d/${file.id}=s1600`);\n      this.log(`Added file: ${file.name}`);\n    });\n\n    return imageUrls;\n  }\n\n  /**\n   * Load specific files by their URLs or IDs\n   * @param fileUrls - Array of Google Drive file URLs or IDs\n   * @param filter - Filter to apply to discovered images\n   * @returns Promise resolving to array of image URLs\n   */\n  private async loadFiles(fileUrls: string[], filter: IImageFilter): Promise<string[]> {\n    const imageUrls: string[] = [];\n\n    for (const fileUrl of fileUrls) {\n      const fileId = this.extractFileId(fileUrl);\n\n      if (!fileId) {\n        this.log(`Skipping invalid file URL: ${fileUrl}`);\n        continue;\n      }\n\n      // Validate it's an image file\n      if (this.apiKey && this.apiKey !== 'YOUR_API_KEY_HERE') {\n        try {\n          // Get file metadata to verify it's an image\n          const metadataUrl = `${this.apiEndpoint}/${fileId}?fields=name,mimeType&key=${this.apiKey}`;\n          const response = await fetch(metadataUrl);\n\n          if (response.ok) {\n            const metadata = await response.json();\n            if (metadata.mimeType.startsWith('image/') && filter.isAllowed(metadata.name)) {\n              imageUrls.push(`https://lh3.googleusercontent.com/d/${fileId}=s1600`);\n              this.log(`Added file: ${metadata.name}`);\n            } else {\n              this.log(`Skipping non-image file: ${metadata.name} (${metadata.mimeType})`);\n            }\n          } else {\n            this.log(`Failed to fetch metadata for file ${fileId}: ${response.status}`);\n          }\n        } catch (error) {\n          this.log(`Error fetching metadata for file ${fileId}:`, error);\n        }\n      } else {\n        // Without API key, assume it's valid and add it\n        imageUrls.push(`https://lh3.googleusercontent.com/d/${fileId}=s1600`);\n      }\n    }\n\n    return imageUrls;\n  }\n\n  /**\n   * Extract file ID from Google Drive file URL\n   * @param fileUrl - Google Drive file URL or file ID\n   * @returns File ID or null if invalid\n   */\n  private extractFileId(fileUrl: string): string | null {\n    // Handle various URL formats:\n    // https://drive.google.com/file/d/FILE_ID/view\n    // https://drive.google.com/open?id=FILE_ID\n    // FILE_ID (raw ID)\n\n    // If it looks like a raw ID (no slashes or protocol), return it\n    if (!/[/:.]/.test(fileUrl)) {\n      return fileUrl;\n    }\n\n    const patterns = [\n      /\\/file\\/d\\/([a-zA-Z0-9_-]+)/,  // Standard file format\n      /\\/open\\?id=([a-zA-Z0-9_-]+)/,  // Alternative format\n      /id=([a-zA-Z0-9_-]+)/           // Generic id parameter\n    ];\n\n    for (const pattern of patterns) {\n      const match = fileUrl.match(pattern);\n      if (match && match[1]) {\n        return match[1];\n      }\n    }\n\n    return null;\n  }\n\n  /**\n   * Recursively load images from a folder and all its subfolders\n   * @param folderId - Google Drive folder ID\n   * @param filter - Filter to apply to discovered images\n   * @returns Promise resolving to array of image URLs\n   */\n  private async loadImagesRecursively(folderId: string, filter: IImageFilter): Promise<string[]> {\n    const imageUrls: string[] = [];\n\n    // Query for all files in this folder\n    const query = `'${folderId}' in parents and trashed=false`;\n    // Request thumbnailLink for PDFs\n    const fields = 'files(id,name,mimeType,thumbnailLink)';\n    const url = `${this.apiEndpoint}?q=${encodeURIComponent(query)}&fields=${fields}&key=${this.apiKey}`;\n\n    const response = await fetch(url);\n\n    if (!response.ok) {\n      throw new Error(`API request failed: ${response.status} ${response.statusText}`);\n    }\n\n    const data: GoogleDriveResponse = await response.json();\n\n    // Separate images and folders using the provided filter\n    const validFiles = data.files.filter(file =>\n      file.mimeType.startsWith('image/') && filter.isAllowed(file.name)\n    );\n\n    const subfolders = data.files.filter(file =>\n      file.mimeType === 'application/vnd.google-apps.folder'\n    );\n\n    this.log(`Found ${data.files.length} total items in folder ${folderId}`);\n    // Log details of all files to see what we are missing\n    data.files.forEach(f => this.log(` - File: ${f.name} (${f.mimeType})`));\n\n    this.log(`- ${validFiles.length} valid files (images only)`);\n    this.log(`- ${subfolders.length} subfolders`);\n\n    // Add image URLs from this folder\n    validFiles.forEach(file => {\n      // Use the reliable thumbnail/preview endpoint for both Images and PDFs\n      // This works for public folders and handles file format conversion automatically\n      // 'sz=w1000' requests a high-quality preview (1000px width)\n      // detailed explanation:\n      // 1. \"drive.google.com\" is blocked by ad-blockers (net::ERR_BLOCKED_BY_CLIENT)\n      // 2. The API's \"thumbnailLink\" is a signed URL that can expire or fail 403.\n      // 3. \"lh3.googleusercontent.com/d/{ID}\" is the permanent CDN link structure.\n      //    It bypasses the domain block AND the signing issues.\n      imageUrls.push(`https://lh3.googleusercontent.com/d/${file.id}=s1600`);\n\n      this.log(`Added file: ${file.name}`);\n    });\n\n    // Recursively process subfolders\n    for (const folder of subfolders) {\n      this.log(`Loading images from subfolder: ${folder.name}`);\n      const subfolderImages = await this.loadImagesRecursively(folder.id, filter);\n      imageUrls.push(...subfolderImages);\n    }\n\n    return imageUrls;\n  }\n\n  /**\n   * Direct loading method (no API key required, but less reliable)\n   * Uses embedded folder view to scrape image IDs\n   * @param folderId - Google Drive folder ID\n   * @param filter - Filter to apply (not used in fallback mode)\n   * @returns Promise resolving to array of image URLs\n   */\n  private async loadImagesDirectly(folderId: string, _filter: IImageFilter): Promise<string[]> {\n    // For now, we'll return a method that requires the user to manually provide image IDs\n    // or we construct URLs based on a known pattern\n\n    // This is a fallback - in production, you'd want to use the API\n    // For demo purposes, we can try to fetch the folder page and extract image IDs\n\n    try {\n      // Attempt to fetch folder page (CORS may block this)\n      const folderUrl = `https://drive.google.com/embeddedfolderview?id=${folderId}`;\n      const response = await fetch(folderUrl, { mode: 'cors' });\n\n      if (!response.ok) {\n        throw new Error('Cannot access folder directly (CORS or permissions issue)');\n      }\n\n      const html = await response.text();\n\n      // Try to extract image IDs from HTML (this is fragile and may break)\n      const imageIdPattern = /\\/file\\/d\\/([a-zA-Z0-9_-]+)/g;\n      const matches = [...html.matchAll(imageIdPattern)];\n      const imageIds = [...new Set(matches.map(m => m[1]))];\n\n      const imageUrls = imageIds.map(id =>\n        `https://drive.google.com/uc?export=view&id=${id}`\n      );\n\n      return imageUrls;\n\n    } catch (error) {\n      console.error('Direct loading failed:', error);\n      throw new Error(\n        'Unable to load images. Please ensure:\\n' +\n        '1. The folder is shared publicly (Anyone with the link can view)\\n' +\n        '2. The folder contains image files\\n' +\n        '3. Consider adding a Google Drive API key in config.js for better reliability'\n      );\n    }\n  }\n\n  /**\n   * Manually add image URLs (for testing or when auto-loading fails)\n   * @param imageIds - Array of Google Drive file IDs\n   * @returns Array of direct image URLs\n   */\n  manualImageUrls(imageIds: string[]): string[] {\n    return imageIds.map(id => `https://drive.google.com/uc?export=view&id=${id}`);\n  }\n\n  /**\n   * Debug logging helper\n   * @param args - Arguments to log\n   */\n  private log(...args: unknown[]): void {\n    if (this.debugLogging && typeof console !== 'undefined') {\n      console.log(...args);\n    }\n  }\n}\n","/**\n * StaticImageLoader.ts\n * Loads images from predefined URL sources and local paths\n * Compatible with ImageCloud's loader interface\n *\n * Public API:\n * - prepare(filter) - Async discovery of images\n * - imagesLength() - Get count of discovered images\n * - imageURLs() - Get ordered list of image URLs\n * - isPrepared() - Check if loader has been prepared\n */\n\nimport type { ImageLoader, IImageFilter, StaticSource, StaticLoaderInnerConfig } from '../config/types';\n\nexport class StaticImageLoader implements ImageLoader {\n  private validateUrls: boolean;\n  private validationTimeout: number;\n  private validationMethod: 'head' | 'simple' | 'none';\n  private sources: StaticSource[];\n  private debugLogging: boolean;\n\n  // State for new interface\n  private _prepared: boolean = false;\n  private _discoveredUrls: string[] = [];\n\n  constructor(config: StaticLoaderInnerConfig) {\n    this.validateUrls = config.validateUrls !== false;\n    this.validationTimeout = config.validationTimeout ?? 5000;\n    this.validationMethod = config.validationMethod ?? 'head';\n    this.debugLogging = config.debugLogging ?? false;\n    this.sources = config.sources ?? [];\n\n    // Validate that we have sources configured\n    if (!this.sources || this.sources.length === 0) {\n      throw new Error('StaticImageLoader requires at least one source to be configured');\n    }\n\n    this.log('StaticImageLoader initialized with config:', config);\n  }\n\n  /**\n   * Prepare the loader by discovering all images from configured sources\n   * @param filter - Filter to apply to discovered images\n   */\n  async prepare(filter: IImageFilter): Promise<void> {\n    this._discoveredUrls = [];\n\n    this.log(`Processing ${this.sources.length} source(s)`);\n\n    // Process sources sequentially to preserve order\n    for (const source of this.sources) {\n      try {\n        const urls = await this.processSource(source, filter);\n        this._discoveredUrls.push(...urls);\n      } catch (error) {\n        console.warn('Failed to process source:', source, error);\n        // Continue processing other sources\n      }\n    }\n\n    this._prepared = true;\n    this.log(`Successfully loaded ${this._discoveredUrls.length} image(s)`);\n  }\n\n  /**\n   * Get the number of discovered images\n   * @throws Error if called before prepare()\n   */\n  imagesLength(): number {\n    if (!this._prepared) {\n      throw new Error('StaticImageLoader.imagesLength() called before prepare()');\n    }\n    return this._discoveredUrls.length;\n  }\n\n  /**\n   * Get the ordered list of image URLs\n   * @throws Error if called before prepare()\n   */\n  imageURLs(): string[] {\n    if (!this._prepared) {\n      throw new Error('StaticImageLoader.imageURLs() called before prepare()');\n    }\n    return [...this._discoveredUrls];\n  }\n\n  /**\n   * Check if the loader has been prepared\n   */\n  isPrepared(): boolean {\n    return this._prepared;\n  }\n\n  /**\n   * Process a single source object using shape-based detection\n   * @param source - Source configuration detected by key presence\n   * @param filter - Filter to apply to discovered images\n   * @returns Promise resolving to array of valid URLs from this source\n   */\n  private async processSource(source: StaticSource, filter: IImageFilter): Promise<string[]> {\n    if (!source) {\n      console.warn('Invalid source object:', source);\n      return [];\n    }\n\n    if ('urls' in source) {\n      return await this.processUrls(source.urls, filter);\n    } else if ('path' in source) {\n      return await this.processPath(source.path, source.files, filter);\n    } else if ('json' in source) {\n      return await this.processJson(source.json, filter);\n    } else {\n      console.warn('Unknown source shape:', source);\n      return [];\n    }\n  }\n\n  /**\n   * Process a list of direct URLs\n   * @param urls - Array of image URLs\n   * @param filter - Filter to apply to discovered images\n   * @returns Promise resolving to array of validated URLs\n   */\n  private async processUrls(urls: string[], filter: IImageFilter): Promise<string[]> {\n    if (!Array.isArray(urls)) {\n      console.warn('URLs must be an array:', urls);\n      return [];\n    }\n\n    const validUrls: string[] = [];\n\n    for (const url of urls) {\n      // Apply filter based on URL filename\n      const filename = url.split('/').pop() || url;\n      if (!filter.isAllowed(filename)) {\n        this.log(`Skipping filtered URL: ${url}`);\n        continue;\n      }\n\n      if (this.validateUrls) {\n        const isValid = await this.validateUrl(url);\n        if (isValid) {\n          validUrls.push(url);\n        } else {\n          console.warn(`Skipping invalid/missing URL: ${url}`);\n        }\n      } else {\n        // No validation - add all URLs\n        validUrls.push(url);\n      }\n    }\n\n    return validUrls;\n  }\n\n  /**\n   * Process a path-based source\n   * @param basePath - Base path (relative or absolute)\n   * @param files - Array of filenames\n   * @param filter - Filter to apply to discovered images\n   * @returns Promise resolving to array of validated URLs\n   */\n  private async processPath(basePath: string, files: string[], filter: IImageFilter): Promise<string[]> {\n\n    if (!Array.isArray(files)) {\n      console.warn('files must be an array:', files);\n      return [];\n    }\n\n    const validUrls: string[] = [];\n\n    for (const file of files) {\n      // Apply filter based on filename\n      if (!filter.isAllowed(file)) {\n        this.log(`Skipping filtered file: ${file}`);\n        continue;\n      }\n\n      const url = this.constructUrl(basePath, file);\n\n      if (this.validateUrls) {\n        const isValid = await this.validateUrl(url);\n        if (isValid) {\n          validUrls.push(url);\n        } else {\n          console.warn(`Skipping invalid/missing file: ${url}`);\n        }\n      } else {\n        // No validation - add all URLs\n        validUrls.push(url);\n      }\n    }\n\n    return validUrls;\n  }\n\n  /**\n   * Process a JSON endpoint source\n   * Fetches a JSON endpoint that returns { images: string[] }\n   * @param url - JSON endpoint URL\n   * @param filter - Filter to apply to discovered images\n   * @returns Promise resolving to array of validated URLs\n   */\n  private async processJson(url: string, filter: IImageFilter): Promise<string[]> {\n\n    this.log(`Fetching JSON endpoint: ${url}`);\n\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => controller.abort(), 10000);\n\n    try {\n      const response = await fetch(url, { signal: controller.signal });\n      clearTimeout(timeoutId);\n\n      if (!response.ok) {\n        throw new Error(`HTTP ${response.status} fetching ${url}`);\n      }\n\n      const data = await response.json();\n\n      if (!data || !Array.isArray(data.images)) {\n        throw new Error(`JSON source must return JSON with shape { \"images\": [\"url1\", \"url2\", ...] }`);\n      }\n\n      this.log(`JSON endpoint returned ${data.images.length} image(s)`);\n\n      // Process the URLs through the standard URL processing pipeline\n      return await this.processUrls(data.images, filter);\n    } catch (error) {\n      clearTimeout(timeoutId);\n      if (error instanceof Error && error.name === 'AbortError') {\n        throw new Error(`Timeout fetching JSON endpoint: ${url}`);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Validate a single URL using HEAD request\n   * @param url - URL to validate\n   * @returns Promise resolving to true if valid and accessible\n   */\n  private async validateUrl(url: string): Promise<boolean> {\n    if (this.validationMethod === 'none') {\n      return true;\n    }\n\n    if (this.validationMethod === 'simple') {\n      // Basic URL format check\n      try {\n        if (typeof window !== 'undefined') {\n          new URL(url, window.location.origin);\n        } else {\n          new URL(url);\n        }\n        return true;\n      } catch {\n        return false;\n      }\n    }\n\n    // validationMethod === 'head' (default)\n    // For cross-origin URLs, we can't validate due to CORS\n    // So we only validate same-origin URLs\n    if (typeof window === 'undefined') {\n      return true; // In non-browser environment, assume valid\n    }\n\n    const isSameOrigin = url.startsWith(window.location.origin) ||\n                         url.startsWith('/');\n\n    if (!isSameOrigin) {\n      // Cross-origin URL - assume valid, can't validate due to CORS\n      this.log(`Skipping validation for cross-origin URL: ${url}`);\n      return true;\n    }\n\n    // Same-origin URL - validate with HEAD request\n    try {\n      const controller = new AbortController();\n      const timeoutId = setTimeout(() => controller.abort(), this.validationTimeout);\n\n      const response = await fetch(url, {\n        method: 'HEAD',\n        signal: controller.signal\n      });\n\n      clearTimeout(timeoutId);\n\n      if (response.ok) {\n        return true;\n      } else {\n        this.log(`Validation failed for ${url}: HTTP ${response.status}`);\n        return false;\n      }\n\n    } catch (error) {\n      if (error instanceof Error) {\n        if (error.name === 'AbortError') {\n          this.log(`Validation timeout for ${url}`);\n        } else {\n          this.log(`Validation failed for ${url}:`, error.message);\n        }\n      }\n      return false;\n    }\n  }\n\n  /**\n   * Construct full URL from basePath and filename\n   * @param basePath - Base path (relative or absolute)\n   * @param filename - Filename to append\n   * @returns Complete URL\n   */\n  private constructUrl(basePath: string, filename: string): string {\n    // Remove trailing slash from basePath\n    const cleanBase = basePath.replace(/\\/$/, '');\n\n    // Check if basePath is absolute URL\n    if (this.isAbsoluteUrl(basePath)) {\n      return `${cleanBase}/${filename}`;\n    }\n\n    // Relative path - prepend current origin\n    if (typeof window === 'undefined') {\n      return `${cleanBase}/${filename}`; // In non-browser environment, return as-is\n    }\n\n    const origin = window.location.origin;\n    // Ensure basePath starts with /\n    const normalizedPath = basePath.startsWith('/') ? basePath : '/' + basePath;\n    const cleanPath = normalizedPath.replace(/\\/$/, '');\n\n    return `${origin}${cleanPath}/${filename}`;\n  }\n\n  /**\n   * Check if URL is absolute (contains protocol)\n   * @param url - URL to check\n   * @returns True if absolute URL\n   */\n  private isAbsoluteUrl(url: string): boolean {\n    try {\n      new URL(url);\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Debug logging helper\n   * @param args - Arguments to log\n   */\n  private log(...args: unknown[]): void {\n    if (this.debugLogging && typeof console !== 'undefined') {\n      console.log(...args);\n    }\n  }\n}\n","/**\n * CompositeLoader.ts\n * Combines multiple image loaders and loads them in parallel\n *\n * Public API:\n * - prepare(filter) - Async discovery of images from all loaders in parallel\n * - imagesLength() - Get combined count of discovered images\n * - imageURLs() - Get combined ordered list of image URLs\n * - isPrepared() - Check if loader has been prepared\n */\n\nimport type { ImageLoader, IImageFilter } from '../config/types';\n\nexport interface CompositeLoaderConfig {\n  loaders: ImageLoader[];\n  debugLogging?: boolean;\n}\n\nexport class CompositeLoader implements ImageLoader {\n  private loaders: ImageLoader[];\n  private debugLogging: boolean;\n\n  // State for interface\n  private _prepared: boolean = false;\n  private _discoveredUrls: string[] = [];\n\n  constructor(config: CompositeLoaderConfig) {\n    this.loaders = config.loaders;\n    this.debugLogging = config.debugLogging ?? false;\n\n    // Validate that we have at least one loader\n    if (!this.loaders || this.loaders.length === 0) {\n      throw new Error('CompositeLoader requires at least one loader to be configured');\n    }\n\n    this.log(`CompositeLoader initialized with ${this.loaders.length} loader(s)`);\n  }\n\n  /**\n   * Prepare all loaders in parallel and combine their results\n   * @param filter - Filter to apply to discovered images\n   */\n  async prepare(filter: IImageFilter): Promise<void> {\n    this._discoveredUrls = [];\n\n    this.log(`Preparing ${this.loaders.length} loader(s) in parallel`);\n\n    // Prepare all loaders in parallel\n    const preparePromises = this.loaders.map((loader, index) => {\n      return loader.prepare(filter).then(() => {\n        this.log(`Loader ${index} prepared with ${loader.imagesLength()} images`);\n      }).catch(error => {\n        console.warn(`Loader ${index} failed to prepare:`, error);\n        // Continue with other loaders even if one fails\n      });\n    });\n\n    await Promise.all(preparePromises);\n\n    // Combine URLs from all prepared loaders (preserves order of loaders array)\n    for (const loader of this.loaders) {\n      if (loader.isPrepared()) {\n        const urls = loader.imageURLs();\n        this._discoveredUrls.push(...urls);\n      }\n    }\n\n    this._prepared = true;\n    this.log(`CompositeLoader prepared with ${this._discoveredUrls.length} total images`);\n  }\n\n  /**\n   * Get the combined number of discovered images\n   * @throws Error if called before prepare()\n   */\n  imagesLength(): number {\n    if (!this._prepared) {\n      throw new Error('CompositeLoader.imagesLength() called before prepare()');\n    }\n    return this._discoveredUrls.length;\n  }\n\n  /**\n   * Get the combined ordered list of image URLs\n   * @throws Error if called before prepare()\n   */\n  imageURLs(): string[] {\n    if (!this._prepared) {\n      throw new Error('CompositeLoader.imageURLs() called before prepare()');\n    }\n    return [...this._discoveredUrls];\n  }\n\n  /**\n   * Check if the loader has been prepared\n   */\n  isPrepared(): boolean {\n    return this._prepared;\n  }\n\n  /**\n   * Debug logging helper\n   * @param args - Arguments to log\n   */\n  private log(...args: unknown[]): void {\n    if (this.debugLogging && typeof console !== 'undefined') {\n      console.log('[CompositeLoader]', ...args);\n    }\n  }\n}\n","/**\n * ImageFilter.ts\n * Filters images by extension, designed for future extensibility\n * (e.g., size filters, date filters, etc.)\n */\n\nexport class ImageFilter {\n  private allowedExtensions: string[];\n\n  /**\n   * Create a new ImageFilter\n   * @param extensions - Array of allowed file extensions (without dots)\n   *                     Defaults to common image formats if not provided\n   */\n  constructor(extensions?: string[]) {\n    this.allowedExtensions = extensions || [\n      'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'\n    ];\n  }\n\n  /**\n   * Check if a filename has an allowed extension\n   * @param filename - The filename to check (can include path or query string)\n   * @returns True if the file extension is allowed\n   */\n  isAllowed(filename: string): boolean {\n    // Remove query string if present (for URLs like image.jpg?w=800)\n    const withoutQuery = filename.split('?')[0];\n    const extension = withoutQuery.split('.').pop()?.toLowerCase();\n    return extension ? this.allowedExtensions.includes(extension) : false;\n  }\n\n  /**\n   * Get the list of allowed extensions\n   * @returns Array of allowed extensions\n   */\n  getAllowedExtensions(): string[] {\n    return [...this.allowedExtensions];\n  }\n\n  // Future expansion methods:\n  // isAllowedSize(sizeBytes: number): boolean\n  // isAllowedDate(date: Date): boolean\n  // isAllowedDimensions(width: number, height: number): boolean\n}\n","/**\n * Minimal functional CSS required for the library to work.\n * Injected automatically - no external CSS file needed.\n */\nexport const FUNCTIONAL_CSS = `\n.fbn-ic-gallery {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  perspective: 1000px;\n}\n\n.fbn-ic-image {\n  position: absolute;\n  cursor: pointer;\n  transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1),\n    box-shadow 0.6s cubic-bezier(0.4, 0, 0.2, 1),\n    filter 0.3s cubic-bezier(0.4, 0, 0.2, 1),\n    opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),\n    border 0.3s cubic-bezier(0.4, 0, 0.2, 1),\n    outline 0.3s cubic-bezier(0.4, 0, 0.2, 1),\n    z-index 0s 0.6s;\n  will-change: transform;\n  user-select: none;\n  backface-visibility: hidden;\n  -webkit-backface-visibility: hidden;\n}\n\n.fbn-ic-image.fbn-ic-focused {\n  z-index: 1000;\n  transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1),\n    box-shadow 0.6s cubic-bezier(0.4, 0, 0.2, 1),\n    filter 0.3s cubic-bezier(0.4, 0, 0.2, 1),\n    opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),\n    border 0.3s cubic-bezier(0.4, 0, 0.2, 1),\n    outline 0.3s cubic-bezier(0.4, 0, 0.2, 1),\n    z-index 0s 0s;\n  will-change: auto;\n}\n\n.fbn-ic-counter {\n  position: fixed;\n  bottom: 24px;\n  left: 50%;\n  transform: translateX(-50%);\n  z-index: 10001;\n  pointer-events: none;\n}\n\n.fbn-ic-gallery:focus,\n.fbn-ic-gallery.fbn-ic-has-focus {\n  outline: 2px solid rgba(147, 197, 253, 0.8);\n  outline-offset: -4px;\n}\n.fbn-ic-gallery.fbn-ic-suppress-outline:focus {\n  outline: none;\n}\n.fbn-ic-gallery.fbn-ic-suppress-outline.fbn-ic-has-focus {\n  outline: 2px solid rgba(99, 102, 241, 0.6);\n  outline-offset: -4px;\n}\n\n.fbn-ic-nav-btn {\n  position: absolute;\n  top: 50%;\n  transform: translateY(-50%);\n  z-index: 10001;\n  cursor: pointer;\n  border: none;\n  background: none;\n  padding: 0;\n  line-height: 1;\n}\n.fbn-ic-nav-btn-prev {\n  left: 12px;\n}\n.fbn-ic-nav-btn-next {\n  right: 12px;\n}\n\n.fbn-ic-hidden {\n  display: none !important;\n}\n`;\n\n/**\n * Inject functional styles into document head.\n * Idempotent - safe to call multiple times.\n */\nexport function injectFunctionalStyles(): void {\n  if (typeof document === 'undefined') return;\n  const id = 'fbn-ic-functional-styles';\n  if (document.getElementById(id)) return;\n  const style = document.createElement('style');\n  style.id = id;\n  style.textContent = FUNCTIONAL_CSS;\n  document.head.appendChild(style);\n}\n","/**\n * ImageCloud.ts\n * Main application class\n * Manages initialization and coordination of the interactive image cloud\n */\n\nimport type { ImageCloudOptions, ImageCloudConfig, ImageLayout, ContainerBounds, ImageLoader, EntryAnimationConfig, LoaderEntry, SharedLoaderConfig, StaticLoaderInnerConfig, GoogleDriveLoaderInnerConfig } from './config/types';\nimport { mergeConfig, DEFAULT_CONFIG } from './config/defaults';\nimport { AnimationEngine } from './engines/AnimationEngine';\nimport { EntryAnimationEngine } from './engines/EntryAnimationEngine';\nimport { IdleAnimationEngine } from './engines/IdleAnimationEngine';\nimport { LayoutEngine } from './engines/LayoutEngine';\nimport { ZoomEngine } from './engines/ZoomEngine';\nimport { SwipeEngine, SNAP_BACK_DURATION_MS } from './engines/SwipeEngine';\nimport { animatePath } from './engines/PathAnimator';\nimport { GoogleDriveLoader } from './loaders/GoogleDriveLoader';\nimport { StaticImageLoader } from './loaders/StaticImageLoader';\nimport { CompositeLoader } from './loaders/CompositeLoader';\nimport { ImageFilter } from './loaders/ImageFilter';\nimport { buildStyleProperties, applyStylesToElementWithState, applyClassNameToElement, removeClassNameFromElement, StyleProperties } from './utils/styleUtils';\nimport { injectFunctionalStyles } from './styles/functionalStyles';\n\nexport class ImageCloud {\n  private containerId: string | null;\n  private containerRef: HTMLElement | null;\n\n  // Internal state\n  private fullConfig: ImageCloudConfig;\n  private imagesLoaded: boolean;\n  private imageElements: HTMLImageElement[];\n  private imageLayouts: ImageLayout[];\n  private currentImageHeight: number;\n  private currentFocusIndex: number | null;\n  private hoveredImage: { element: HTMLImageElement; layout: ImageLayout } | null;\n  private resizeTimeout: number | null;\n  private displayQueue: HTMLImageElement[];\n  private queueInterval: number | null;\n  private loadGeneration: number;\n\n  // Precomputed styling\n  private defaultStyles: StyleProperties;\n  private defaultClassName: string | string[] | undefined;\n  private hoverClassName: string | string[] | undefined;\n\n  // Modules\n  private animationEngine: AnimationEngine;\n  private entryAnimationEngine: EntryAnimationEngine;\n  private idleAnimationEngine: IdleAnimationEngine | null;\n  private layoutEngine: LayoutEngine;\n  private zoomEngine: ZoomEngine;\n  private swipeEngine: SwipeEngine | null;\n  private imageLoader: ImageLoader;\n  private imageFilter: ImageFilter;\n\n  // DOM Elements\n  private containerEl: HTMLElement | null;\n  private loadingEl: HTMLElement | null;\n  private errorEl: HTMLElement | null;\n  private loadingElAutoCreated: boolean;\n  private errorElAutoCreated: boolean;\n  private counterEl: HTMLElement | null;\n  private counterElAutoCreated: boolean;\n  private prevButtonEl: HTMLElement | null;\n  private nextButtonEl: HTMLElement | null;\n  private prevButtonElAutoCreated: boolean;\n  private nextButtonElAutoCreated: boolean;\n\n  constructor(options: ImageCloudOptions = {}) {\n    this.fullConfig = mergeConfig(options);\n\n    // Container can be a string ID or an HTMLElement reference\n    if (options.container instanceof HTMLElement) {\n      this.containerRef = options.container;\n      this.containerId = null;\n    } else {\n      this.containerRef = null;\n      this.containerId = options.container || 'imageCloud';\n    }\n\n    // Internal state\n    this.imagesLoaded = false;\n    this.imageElements = [];\n    this.imageLayouts = [];\n    this.currentImageHeight = 225;\n    this.currentFocusIndex = null;\n    this.hoveredImage = null;\n    this.resizeTimeout = null;\n    this.displayQueue = [];\n    this.queueInterval = null;\n    this.loadGeneration = 0;\n    this.loadingElAutoCreated = false;\n    this.errorElAutoCreated = false;\n    this.counterEl = null;\n    this.counterElAutoCreated = false;\n    this.prevButtonEl = null;\n    this.nextButtonEl = null;\n    this.prevButtonElAutoCreated = false;\n    this.nextButtonElAutoCreated = false;\n\n    // Initialize engines with new config structure\n    this.animationEngine = new AnimationEngine(this.fullConfig.animation);\n    this.layoutEngine = new LayoutEngine({\n      layout: this.fullConfig.layout,\n      image: this.fullConfig.image\n    });\n    this.zoomEngine = new ZoomEngine(this.fullConfig.interaction.focus, this.animationEngine, this.fullConfig.styling);\n\n    // Precompute styling properties\n    this.defaultStyles = buildStyleProperties(this.fullConfig.styling?.default);\n    this.defaultClassName = this.fullConfig.styling?.default?.className;\n    this.hoverClassName = this.fullConfig.styling?.hover?.className;\n\n    // Initialize entry animation engine with layout-aware defaults\n    const entryConfig = this.fullConfig.animation.entry || DEFAULT_CONFIG.animation.entry!;\n    this.entryAnimationEngine = new EntryAnimationEngine(\n      entryConfig as EntryAnimationConfig,\n      this.fullConfig.layout.algorithm\n    );\n\n    // Initialize idle animation engine if configured\n    const idleConfig = this.fullConfig.animation.idle;\n    if (idleConfig && idleConfig.type !== 'none') {\n      this.idleAnimationEngine = new IdleAnimationEngine(\n        idleConfig,\n        (entryConfig as EntryAnimationConfig).timing?.duration ?? 600\n      );\n    } else {\n      this.idleAnimationEngine = null;\n    }\n\n    // Wire unfocus complete callback to resume idle animations and re-apply hover styles\n    this.zoomEngine.setOnUnfocusCompleteCallback((el) => {\n      this.idleAnimationEngine?.resumeForImage(el as HTMLImageElement);\n      // If the cursor is still over this image, mouseenter won't re-fire — re-apply hover styles.\n      // Defer to next frame so the browser updates :hover after the animation finishes.\n      const img = el as HTMLImageElement;\n      requestAnimationFrame(() => {\n        if (img.matches(':hover') && this.fullConfig.styling?.hover) {\n          const idx = this.imageElements.indexOf(img);\n          if (idx !== -1) {\n            const imageHeight = img.offsetHeight;\n            const cachedWidth = (img as any).cachedRenderedWidth;\n            applyStylesToElementWithState(img, this.fullConfig.styling.hover, imageHeight, cachedWidth);\n            applyClassNameToElement(img, this.hoverClassName);\n            this.hoveredImage = { element: img, layout: this.imageLayouts[idx] };\n          }\n        }\n      });\n    });\n\n    // SwipeEngine will be initialized after container is available\n    this.swipeEngine = null;\n\n    // Initialize image filter with configured extensions\n    this.imageFilter = this.createImageFilter();\n\n    // Initialize image loader based on type\n    this.imageLoader = this.createLoader();\n\n    // DOM Elements (will be fetched on init)\n    this.containerEl = null;\n    this.loadingEl = null;\n    this.errorEl = null;\n  }\n\n  /**\n   * Create image filter based on shared loader config\n   */\n  private createImageFilter(): ImageFilter {\n    const extensions = this.fullConfig.config.loaders?.allowedExtensions;\n    return new ImageFilter(extensions);\n  }\n\n  /**\n   * Create appropriate image loader based on config\n   * Processes loaders array, merges shared config, wraps in CompositeLoader if needed\n   */\n  private createLoader(): ImageLoader {\n    const entries = this.fullConfig.loaders;\n    const shared = this.fullConfig.config.loaders ?? {};\n\n    if (!entries || entries.length === 0) {\n      throw new Error('No loaders configured. Provide `images`, `loaders`, or both.\\n  Example: imageCloud({ container: \\'id\\', images: [\\'https://...\\'] })');\n    }\n\n    const childLoaders = entries.map(entry => this.createLoaderFromEntry(entry, shared));\n\n    if (childLoaders.length === 1) {\n      return childLoaders[0];\n    }\n\n    return new CompositeLoader({\n      loaders: childLoaders,\n      debugLogging: this.fullConfig.config.debug?.loaders\n    });\n  }\n\n  /**\n   * Create a single loader from a LoaderEntry, merging shared config\n   */\n  private createLoaderFromEntry(entry: LoaderEntry, shared: SharedLoaderConfig): ImageLoader {\n    if ('static' in entry) {\n      const inner = entry.static;\n      const merged: StaticLoaderInnerConfig = {\n        ...inner,\n        validateUrls: inner.validateUrls ?? shared.validateUrls,\n        validationTimeout: inner.validationTimeout ?? shared.validationTimeout,\n        validationMethod: inner.validationMethod ?? shared.validationMethod,\n        allowedExtensions: inner.allowedExtensions ?? shared.allowedExtensions,\n        debugLogging: inner.debugLogging ?? this.fullConfig.config.debug?.loaders\n      };\n      return new StaticImageLoader(merged);\n    } else if ('googleDrive' in entry) {\n      const inner = entry.googleDrive;\n      const merged: GoogleDriveLoaderInnerConfig = {\n        ...inner,\n        allowedExtensions: inner.allowedExtensions ?? shared.allowedExtensions,\n        debugLogging: inner.debugLogging ?? this.fullConfig.config.debug?.loaders\n      };\n      return new GoogleDriveLoader(merged);\n    } else {\n      throw new Error(`Unknown loader entry: ${JSON.stringify(entry)}`);\n    }\n  }\n\n  /**\n   * Initialize the gallery\n   */\n  async init(): Promise<void> {\n    try {\n      // Inject functional styles (idempotent)\n      injectFunctionalStyles();\n\n      // 1. Setup DOM\n      if (this.containerRef) {\n        this.containerEl = this.containerRef;\n      } else {\n        this.containerEl = document.getElementById(this.containerId!);\n        if (!this.containerEl) {\n          throw new Error(`Container \"#${this.containerId}\" not found. Ensure an element with id=\"${this.containerId}\" exists in the DOM before calling imageCloud().`);\n        }\n      }\n\n      // Add gallery class for CSS scoping\n      this.containerEl.classList.add('fbn-ic-gallery');\n      this.containerEl.setAttribute('tabindex', '0');\n\n      // Initialize swipe engine for touch navigation (guarded by config flag)\n      if (this.fullConfig.interaction.navigation?.swipe !== false) {\n        this.swipeEngine = new SwipeEngine(this.containerEl, {\n          onNext: () => this.navigateToNextImage(),\n          onPrev: () => this.navigateToPreviousImage(),\n          onDragOffset: (offset) => this.zoomEngine.setDragOffset(offset),\n          onDragEnd: (navigated) => {\n            if (!navigated) {\n              // Snap back to center with animation\n              this.zoomEngine.clearDragOffset(true, SNAP_BACK_DURATION_MS);\n            } else {\n              // Clear offset immediately (navigation handles transition)\n              this.zoomEngine.clearDragOffset(false);\n            }\n          }\n        });\n      }\n\n      // Create or bind UI elements\n      this.setupUI();\n\n      // 2. Setup Event Listeners\n      this.setupEventListeners();\n\n      // 3. Load Images\n      this.logDebug('ImageCloud initialized');\n      await this.loadImages();\n\n    } catch (error) {\n      console.error(`ImageCloud initialization failed (container: \"${this.containerId ?? 'element'}\"):`, error);\n      if (this.errorEl && error instanceof Error) {\n        this.showError('Gallery failed to initialize: ' + error.message);\n      }\n    }\n  }\n\n  private setupUI(): void {\n    const uiConfig = this.fullConfig.ui;\n\n    // Manage focus outline: suppress browser ring by default, restore when showFocusOutline: true\n    if (!uiConfig.showFocusOutline) {\n      this.containerEl?.classList.add('fbn-ic-suppress-outline');\n    } else {\n      this.containerEl?.classList.remove('fbn-ic-suppress-outline');\n    }\n\n    // Loading element\n    if (uiConfig.showLoadingSpinner) {\n      if (uiConfig.loadingElement) {\n        this.loadingEl = this.resolveElement(uiConfig.loadingElement);\n        this.loadingElAutoCreated = false;\n      } else {\n        this.loadingEl = this.createDefaultLoadingElement();\n        this.loadingElAutoCreated = true;\n      }\n    }\n\n    // Error element\n    if (uiConfig.errorElement) {\n      this.errorEl = this.resolveElement(uiConfig.errorElement);\n      this.errorElAutoCreated = false;\n    } else {\n      this.errorEl = this.createDefaultErrorElement();\n      this.errorElAutoCreated = true;\n    }\n\n    // Counter element\n    if (uiConfig.showImageCounter) {\n      if (uiConfig.counterElement) {\n        this.counterEl = this.resolveElement(uiConfig.counterElement);\n        this.counterElAutoCreated = false;\n      } else {\n        this.counterEl = this.createDefaultCounterElement();\n        this.counterElAutoCreated = true;\n      }\n    }\n\n    // Nav button elements\n    if (uiConfig.showNavButtons) {\n      if (uiConfig.prevButtonElement) {\n        this.prevButtonEl = this.resolveElement(uiConfig.prevButtonElement);\n        this.prevButtonElAutoCreated = false;\n      } else {\n        this.prevButtonEl = this.createDefaultPrevButtonElement();\n        this.prevButtonElAutoCreated = true;\n      }\n      if (uiConfig.nextButtonElement) {\n        this.nextButtonEl = this.resolveElement(uiConfig.nextButtonElement);\n        this.nextButtonElAutoCreated = false;\n      } else {\n        this.nextButtonEl = this.createDefaultNextButtonElement();\n        this.nextButtonElAutoCreated = true;\n      }\n      this.prevButtonEl?.addEventListener('click', (e) => {\n        e.stopPropagation();\n        this.navigateToPreviousImage();\n      });\n      this.nextButtonEl?.addEventListener('click', (e) => {\n        e.stopPropagation();\n        this.navigateToNextImage();\n      });\n    }\n  }\n\n  private resolveElement(ref: string | HTMLElement): HTMLElement | null {\n    if (ref instanceof HTMLElement) return ref;\n    return document.getElementById(ref);\n  }\n\n  private createDefaultLoadingElement(): HTMLElement {\n    const el = document.createElement('div');\n    el.className = 'fbn-ic-loading fbn-ic-hidden';\n    const spinner = document.createElement('div');\n    spinner.className = 'fbn-ic-spinner';\n    el.appendChild(spinner);\n    const text = document.createElement('p');\n    text.textContent = 'Loading images...';\n    el.appendChild(text);\n    this.containerEl!.appendChild(el);\n    return el;\n  }\n\n  private createDefaultErrorElement(): HTMLElement {\n    const el = document.createElement('div');\n    el.className = 'fbn-ic-error fbn-ic-hidden';\n    this.containerEl!.appendChild(el);\n    return el;\n  }\n\n  private createDefaultCounterElement(): HTMLElement {\n    const el = document.createElement('div');\n    el.className = 'fbn-ic-counter fbn-ic-hidden';\n    this.containerEl!.appendChild(el);\n    return el;\n  }\n\n  private createDefaultPrevButtonElement(): HTMLElement {\n    const el = document.createElement('button');\n    el.className = 'fbn-ic-nav-btn fbn-ic-nav-btn-prev fbn-ic-hidden';\n    el.textContent = '‹';\n    el.setAttribute('aria-label', 'Previous image');\n    el.setAttribute('tabindex', '-1');\n    this.containerEl!.appendChild(el);\n    return el;\n  }\n\n  private createDefaultNextButtonElement(): HTMLElement {\n    const el = document.createElement('button');\n    el.className = 'fbn-ic-nav-btn fbn-ic-nav-btn-next fbn-ic-hidden';\n    el.textContent = '›';\n    el.setAttribute('aria-label', 'Next image');\n    el.setAttribute('tabindex', '-1');\n    this.containerEl!.appendChild(el);\n    return el;\n  }\n\n  private setupEventListeners(): void {\n    // Keyboard navigation — scoped to container, guarded by config flag\n    if (this.fullConfig.interaction.navigation?.keyboard !== false) {\n      this.containerEl!.addEventListener('keydown', (e: KeyboardEvent) => {\n        if (e.key === 'Escape') {\n          this.zoomEngine.unfocusImage();\n          this.currentFocusIndex = null;\n          this.swipeEngine?.disable();\n          this.hideCounter();\n          this.hideNavButtons();\n          this.hideFocusIndicator();\n        } else if (e.key === 'ArrowRight') {\n          this.navigateToNextImage();\n        } else if (e.key === 'ArrowLeft') {\n          this.navigateToPreviousImage();\n        } else if ((e.key === 'Enter' || e.key === ' ') && this.hoveredImage) {\n          this.handleImageClick(this.hoveredImage.element, this.hoveredImage.layout);\n          e.preventDefault();\n        }\n      });\n    }\n\n    document.addEventListener('click', (e: MouseEvent) => {\n      // Ignore clicks that follow touch events (prevents unfocus during swipe)\n      if (this.swipeEngine?.hadRecentTouch()) {\n        return;\n      }\n      if (!(e.target as HTMLElement).closest('.fbn-ic-image') &&\n          !(e.target as HTMLElement).closest('.fbn-ic-nav-btn')) {\n        this.zoomEngine.unfocusImage();\n        this.currentFocusIndex = null;\n        this.swipeEngine?.disable();\n        this.hideCounter();\n        this.hideNavButtons();\n        this.hideFocusIndicator();\n      }\n    });\n\n    // Resize handler\n    window.addEventListener('resize', () => this.handleResize());\n  }\n\n  /**\n   * Navigate to the next image (Right arrow)\n   */\n  private navigateToNextImage(): void {\n    if (this.currentFocusIndex === null || this.imageElements.length === 0) return;\n\n    const nextId = (this.currentFocusIndex + 1) % this.imageLayouts.length;\n    const nextElement = this.imageElements.find(\n      el => el.dataset.imageId === String(nextId)\n    );\n    if (!nextElement) return;\n\n    const layout = this.imageLayouts[nextId];\n    if (!layout) return;\n\n    this.currentFocusIndex = nextId;\n    this.handleImageClick(nextElement, layout);\n    this.updateCounter(nextId);\n    this.showNavButtons();\n    this.showFocusIndicator();\n  }\n\n  /**\n   * Navigate to the previous image (Left arrow)\n   */\n  private navigateToPreviousImage(): void {\n    if (this.currentFocusIndex === null || this.imageElements.length === 0) return;\n\n    const prevId = (this.currentFocusIndex - 1 + this.imageLayouts.length) % this.imageLayouts.length;\n    const prevElement = this.imageElements.find(\n      el => el.dataset.imageId === String(prevId)\n    );\n    if (!prevElement) return;\n\n    const layout = this.imageLayouts[prevId];\n    if (!layout) return;\n\n    this.currentFocusIndex = prevId;\n    this.handleImageClick(prevElement, layout);\n    this.updateCounter(prevId);\n    this.showNavButtons();\n    this.showFocusIndicator();\n  }\n\n  /**\n   * Navigate to a specific image by index\n   */\n  private handleResize(): void {\n    if (!this.imagesLoaded) return;\n\n    if (this.resizeTimeout !== null) {\n      clearTimeout(this.resizeTimeout);\n    }\n\n    this.resizeTimeout = window.setTimeout(() => {\n      const newHeight = this.getImageHeight();\n\n      if (newHeight !== this.currentImageHeight) {\n        this.logDebug(`Window resized to new breakpoint (height: ${newHeight}px). Reloading images...`);\n        // Reload images with new breakpoint\n        this.loadImages();\n      } else {\n        this.logDebug('Window resized (no breakpoint change)');\n      }\n    }, 500);\n  }\n\n  private getImageHeight(): number {\n    const width = window.innerWidth;\n    const responsive = this.fullConfig.layout.responsive;\n\n    // Get sizing config for adaptive mode defaults\n    const sizing = this.fullConfig.image.sizing;\n    const maxSize = sizing?.maxSize ?? 400;\n\n    // Use responsive breakpoints to determine max height\n    // These serve as upper bounds for the adaptive sizing\n    if (!responsive) {\n      // Fallback defaults if responsive not configured\n      if (width <= 767) return Math.min(100, maxSize);\n      if (width <= 1199) return Math.min(180, maxSize);\n      return Math.min(225, maxSize);\n    }\n\n    if (width <= responsive.mobile.maxWidth) {\n      return Math.min(100, maxSize);  // Mobile\n    }\n    if (width <= responsive.tablet.maxWidth) {\n      return Math.min(180, maxSize);  // Tablet\n    }\n    return Math.min(225, maxSize);  // Screen (desktop)\n  }\n\n  /**\n   * Get container bounds for layout calculations\n   */\n  private getContainerBounds(): { width: number; height: number } {\n    if (!this.containerEl) {\n      return { width: window.innerWidth, height: window.innerHeight * 0.7 };\n    }\n    return {\n      width: this.containerEl.offsetWidth,\n      height: this.containerEl.offsetHeight || window.innerHeight * 0.7\n    };\n  }\n\n  /**\n   * Load images using the unified loader interface\n   */\n  private async loadImages(): Promise<void> {\n    try {\n      this.showLoading(true);\n      this.hideError();\n      this.clearImageCloud();\n\n      // Prepare the loader (show spinner during this)\n      await this.imageLoader.prepare(this.imageFilter);\n\n      // Get image count and URLs from loader\n      const imageCount = this.imageLoader.imagesLength();\n      let imageUrls = this.imageLoader.imageURLs();\n\n      if (imageCount === 0) {\n        this.showError('No images found.');\n        this.showLoading(false);\n        return;\n      }\n\n      // Calculate adaptive sizing based on container and image count\n      const containerBounds = this.getContainerBounds();\n      const responsiveHeight = this.getImageHeight();\n      const viewportWidth = window.innerWidth;\n\n      this.logDebug(`Adaptive sizing input: container=${containerBounds.width}x${containerBounds.height}px, images=${imageCount}, responsiveMax=${responsiveHeight}px`);\n\n      const sizingResult = this.layoutEngine.calculateAdaptiveSize(\n        containerBounds,\n        imageCount,\n        responsiveHeight,\n        viewportWidth\n      );\n\n      this.logDebug(`Adaptive sizing result: height=${sizingResult.height}px`);\n\n      await this.createImageCloud(imageUrls, sizingResult.height);\n\n      this.showLoading(false);\n      this.imagesLoaded = true;\n\n    } catch (error) {\n      console.error('Error loading images:', error);\n      if (error instanceof Error) {\n        this.showError(error.message || 'Failed to load images.');\n      }\n      this.showLoading(false);\n    }\n  }\n\n  /**\n   * Helper for debug logging\n   */\n  private logDebug(...args: unknown[]): void {\n    if (this.fullConfig.config.debug?.enabled && typeof console !== 'undefined') {\n      console.log(...args);\n    }\n  }\n\n  private async createImageCloud(imageUrls: string[], imageHeight: number): Promise<void> {\n    if (!this.containerEl) return;\n\n    const containerBounds = this.getContainerBounds();\n    this.currentImageHeight = imageHeight;\n\n    // Capture current generation to detect stale callbacks\n    const currentGeneration = this.loadGeneration;\n\n    // Generate layout\n    const layouts = this.layoutEngine.generateLayout(imageUrls.length, containerBounds, { fixedHeight: imageHeight } as any);\n    this.imageLayouts = layouts;\n\n    this.displayQueue = [];\n    let processedCount = 0;\n\n    // Helper to display a single image with animation\n    const displayImage = (img: HTMLImageElement) => {\n      if (!this.containerEl) return;\n\n      this.containerEl.appendChild(img);\n      this.imageElements.push(img);\n\n      requestAnimationFrame(() => {\n        void img.offsetWidth; // Force reflow\n        // Use configured default opacity, or 1 if not specified\n        img.style.opacity = this.defaultStyles.opacity ?? '1';\n\n        // Check if we need JS animation for path type, rotation, or scale\n        const needsJSAnimation = img.dataset.startX &&\n          (this.entryAnimationEngine.requiresJSAnimation() ||\n           this.entryAnimationEngine.requiresJSRotation() ||\n           this.entryAnimationEngine.requiresJSScale() ||\n           img.dataset.startRotation !== img.dataset.rotation ||\n           img.dataset.startScale !== img.dataset.scale);\n\n        if (needsJSAnimation) {\n          // Use animatePath for bounce, elastic, wave paths or rotation/scale animation\n          const startPosition = {\n            x: parseFloat(img.dataset.startX!),\n            y: parseFloat(img.dataset.startY!)\n          };\n          const endPosition = {\n            x: parseFloat(img.dataset.endX!),\n            y: parseFloat(img.dataset.endY!)\n          };\n          const imageWidth = parseFloat(img.dataset.imageWidth!);\n          const imageHeight = parseFloat(img.dataset.imageHeight!);\n          const rotation = parseFloat(img.dataset.rotation!);\n          const scale = parseFloat(img.dataset.scale!);\n          const startRotation = img.dataset.startRotation\n            ? parseFloat(img.dataset.startRotation)\n            : rotation;\n          const startScale = img.dataset.startScale\n            ? parseFloat(img.dataset.startScale)\n            : scale;\n          const timing = this.entryAnimationEngine.getTiming();\n\n          animatePath({\n            element: img,\n            startPosition,\n            endPosition,\n            pathConfig: this.entryAnimationEngine.getPathConfig(),\n            duration: timing.duration,\n            imageWidth,\n            imageHeight,\n            rotation,\n            scale,\n            rotationConfig: this.entryAnimationEngine.getRotationConfig(),\n            startRotation,\n            scaleConfig: this.entryAnimationEngine.getScaleConfig(),\n            startScale\n          });\n        } else {\n          // Use CSS transition for linear/arc paths without rotation animation\n          const finalTransform = img.dataset.finalTransform || '';\n          img.style.transform = finalTransform;\n        }\n\n        // Debug: log final state for first few images\n        const imgIndex = parseInt(img.dataset.imageId || '0');\n        if (this.fullConfig.config.debug?.enabled && imgIndex < 3) {\n          const finalTransform = img.dataset.finalTransform || '';\n          console.log(`Image ${imgIndex} final state:`, {\n            left: img.style.left,\n            top: img.style.top,\n            width: img.style.width,\n            height: img.style.height,\n            computedWidth: img.offsetWidth,\n            computedHeight: img.offsetHeight,\n            transform: finalTransform,\n            pathType: this.entryAnimationEngine.getPathType()\n          });\n        }\n\n        // Register with idle animation engine (starts after entry completes)\n        if (this.idleAnimationEngine) {\n          const entryDuration = this.entryAnimationEngine.getTiming().duration;\n          this.idleAnimationEngine.register(img, imgIndex, this.imageElements.length, entryDuration);\n        }\n      });\n\n      processedCount++;\n    };\n\n    const startQueueProcessing = () => {\n      this.logDebug('Starting queue processing, enabled:', this.fullConfig.animation.queue.enabled);\n\n      // If queue is disabled, display all images immediately\n      if (!this.fullConfig.animation.queue.enabled) {\n        while (this.displayQueue.length > 0) {\n          const img = this.displayQueue.shift();\n          if (img) {\n            displayImage(img);\n          }\n        }\n        return;\n      }\n\n      // Queue is enabled - stagger images with interval\n      // Clear any existing interval before creating new one\n      if (this.queueInterval !== null) {\n        clearInterval(this.queueInterval);\n      }\n      this.queueInterval = window.setInterval(() => {\n        // Check if this interval is still valid (generation hasn't changed)\n        if (currentGeneration !== this.loadGeneration) {\n          if (this.queueInterval !== null) {\n            clearInterval(this.queueInterval);\n            this.queueInterval = null;\n          }\n          return;\n        }\n\n        if (this.displayQueue.length > 0) {\n          const img = this.displayQueue.shift();\n          if (img) {\n            displayImage(img);\n          }\n        }\n\n        if (processedCount >= imageUrls.length && this.displayQueue.length === 0) {\n          if (this.queueInterval !== null) {\n            clearInterval(this.queueInterval);\n            this.queueInterval = null;\n          }\n        }\n      }, this.fullConfig.animation.queue.interval);\n    };\n\n    // Visibility Check\n    if ('IntersectionObserver' in window && this.containerEl) {\n      const observer = new IntersectionObserver((entries) => {\n        entries.forEach(entry => {\n          if (entry.isIntersecting) {\n            startQueueProcessing();\n            observer.disconnect();\n          }\n        });\n      }, { threshold: 0.1, rootMargin: '50px' });\n      observer.observe(this.containerEl);\n    } else {\n      startQueueProcessing();\n    }\n\n    // Debug: Draw center markers if debug.centers is enabled\n    if (this.fullConfig.config.debug?.centers && this.containerEl) {\n      // Remove any existing debug markers\n      this.containerEl.querySelectorAll('.fbn-ic-debug-center').forEach(el => el.remove());\n\n      layouts.forEach((layout, index) => {\n        const marker = document.createElement('div');\n        marker.className = 'fbn-ic-debug-center';\n        marker.style.position = 'absolute';\n        marker.style.width = '12px';\n        marker.style.height = '12px';\n        marker.style.borderRadius = '50%';\n        marker.style.backgroundColor = 'red';\n        marker.style.border = '2px solid yellow';\n        marker.style.zIndex = '9999';\n        marker.style.pointerEvents = 'none';\n        // Center position: layout.x and layout.y now store the center position directly\n        const centerX = layout.x;\n        const centerY = layout.y;\n        marker.style.left = `${centerX - 6}px`;  // Offset by half marker size\n        marker.style.top = `${centerY - 6}px`;\n        marker.title = `Image ${index}: center (${Math.round(centerX)}, ${Math.round(centerY)})`;\n        this.containerEl!.appendChild(marker);\n      });\n    }\n\n    // Create elements\n    imageUrls.forEach((url, index) => {\n      const img = document.createElement('img');\n      // NOTE: img.src is set AFTER onload handler to ensure handler catches cached images\n      img.referrerPolicy = 'no-referrer';\n      img.classList.add('fbn-ic-image');\n      if (this.fullConfig.interaction.dragging === false) {\n        img.draggable = false;\n      }\n      img.dataset.imageId = String(index);\n      img.dataset.createdFlag = 'true';  // Debug flag\n\n      const layout = layouts[index];\n      img.style.position = 'absolute';\n      img.style.width = 'auto';\n      img.style.height = `${imageHeight}px`;\n      img.style.left = `${layout.x}px`;\n      img.style.top = `${layout.y}px`;\n      // Transform will be applied in onload after we know the actual dimensions\n\n      if (layout.zIndex) img.style.zIndex = String(layout.zIndex);\n\n      // NOTE: Default styling will be applied in onload after image dimensions are known\n      // This ensures height-relative clip-path is calculated correctly with proper width\n      // Element starts with opacity 0 so it's not visible until onload completes\n      applyClassNameToElement(img, this.defaultClassName);\n\n      // Hover event handlers\n      // Use isInvolved() to prevent hover styles on images that are focused or animating\n      img.addEventListener('mouseenter', () => {\n        this.hoveredImage = { element: img, layout };\n        if (!this.zoomEngine.isInvolved(img)) {\n          // Use cached rendered width for consistent clip-path centering (prevents shifting)\n          const cachedWidth = (img as any).cachedRenderedWidth;\n          applyStylesToElementWithState(img, this.fullConfig.styling?.hover, imageHeight, cachedWidth);\n          applyClassNameToElement(img, this.hoverClassName);\n        }\n      });\n\n      img.addEventListener('mouseleave', () => {\n        this.hoveredImage = null;\n        if (!this.zoomEngine.isInvolved(img)) {\n          // Use cached rendered width for consistent clip-path centering (prevents shifting)\n          const cachedWidth = (img as any).cachedRenderedWidth;\n          applyStylesToElementWithState(img, this.fullConfig.styling?.default, imageHeight, cachedWidth);\n          removeClassNameFromElement(img, this.hoverClassName);\n          applyClassNameToElement(img, this.defaultClassName);\n        }\n      });\n\n      img.addEventListener('click', (e: MouseEvent) => {\n        e.stopPropagation();\n        this.handleImageClick(img, layout);\n      });\n\n      img.style.opacity = '0';\n      img.style.transition = this.entryAnimationEngine.getTransitionCSS();\n\n      img.onload = () => {\n        // Ignore if generation has changed (stale callback from previous load)\n        if (currentGeneration !== this.loadGeneration) {\n          return;\n        }\n\n        const aspectRatio = img.naturalWidth / img.naturalHeight;\n        const renderedWidth = imageHeight * aspectRatio;\n\n        // Debug: mark that onload was called\n        img.dataset.onloadCalled = 'true';\n        if ((window as any).DEBUG_CLIPPATH) {\n          console.log(`[onload #${index}] Called with imageHeight=${imageHeight}, renderedWidth=${renderedWidth}`);\n        }\n\n        // Set explicit width so transform calculations are accurate\n        img.style.width = `${renderedWidth}px`;\n\n        // Store rendered width and aspect ratio on element for use in event handlers and focused state\n        (img as any).cachedRenderedWidth = renderedWidth;\n        (img as any).aspectRatio = aspectRatio;\n\n        // Reapply default styling with correct width for height-relative clip-path centering\n        // Now we know both height and the rendered width (from aspect ratio)\n        applyStylesToElementWithState(img, this.fullConfig.styling?.default, imageHeight, renderedWidth);\n\n        // Use EntryAnimationEngine for start position calculation\n        const finalPosition = { x: layout.x, y: layout.y };\n        const imageSize = { width: renderedWidth, height: imageHeight };\n\n        const startPosition = this.entryAnimationEngine.calculateStartPosition(\n          finalPosition,\n          imageSize,\n          containerBounds,\n          index,\n          imageUrls.length\n        );\n\n        // Calculate start rotation based on entry rotation config\n        const startRotation = this.entryAnimationEngine.calculateStartRotation(layout.rotation);\n\n        // Calculate start scale based on entry scale config\n        const startScale = this.entryAnimationEngine.calculateStartScale(layout.scale);\n\n        const finalTransform = this.entryAnimationEngine.buildFinalTransform(\n          layout.rotation,\n          layout.scale,\n          renderedWidth,\n          imageHeight\n        );\n        const startTransform = this.entryAnimationEngine.buildStartTransform(\n          startPosition,\n          finalPosition,\n          layout.rotation,\n          layout.scale,\n          renderedWidth,\n          imageHeight,\n          startRotation,\n          startScale\n        );\n\n        if (this.fullConfig.config.debug?.enabled && index < 3) {\n          console.log(`Image ${index}:`, {\n            finalPosition,\n            imageSize,\n            left: layout.x,\n            top: layout.y,\n            finalTransform,\n            renderedWidth,\n            renderedHeight: imageHeight\n          });\n        }\n\n        img.style.transform = startTransform;\n        img.dataset.finalTransform = finalTransform;\n\n        // Store animation data for JS-animated paths (bounce, elastic, wave)\n        // or when rotation/scale animation is needed\n        const needsJSAnimation = this.entryAnimationEngine.requiresJSAnimation() ||\n          this.entryAnimationEngine.requiresJSRotation() ||\n          this.entryAnimationEngine.requiresJSScale() ||\n          startRotation !== layout.rotation ||\n          startScale !== layout.scale;\n\n        if (needsJSAnimation) {\n          img.dataset.startX = String(startPosition.x);\n          img.dataset.startY = String(startPosition.y);\n          img.dataset.endX = String(finalPosition.x);\n          img.dataset.endY = String(finalPosition.y);\n          img.dataset.imageWidth = String(renderedWidth);\n          img.dataset.imageHeight = String(imageHeight);\n          img.dataset.rotation = String(layout.rotation);\n          img.dataset.scale = String(layout.scale);\n          img.dataset.startRotation = String(startRotation);\n          img.dataset.startScale = String(startScale);\n        }\n\n        this.displayQueue.push(img);\n      };\n\n      img.onerror = () => processedCount++;\n\n      // Set src AFTER onload handler to ensure it catches cached images\n      img.src = url;\n    });\n  }\n\n  private async handleImageClick(imageElement: HTMLImageElement, originalLayout: ImageLayout): Promise<void> {\n    if (!this.containerEl) return;\n\n    const isFocused = this.zoomEngine.isFocused(imageElement);\n    const bounds: ContainerBounds = {\n      width: this.containerEl.offsetWidth,\n      height: this.containerEl.offsetHeight\n    };\n\n    if (isFocused) {\n      await this.zoomEngine.unfocusImage();\n      this.currentFocusIndex = null;\n      this.swipeEngine?.disable();\n      this.hideCounter();\n      this.hideNavButtons();\n      this.hideFocusIndicator();\n    } else {\n      // Pause idle animation immediately before focus animation begins\n      this.idleAnimationEngine?.pauseForImage(imageElement);\n\n      // Track the focused image index for keyboard navigation\n      const imageId = imageElement.dataset.imageId;\n      this.currentFocusIndex = imageId !== undefined ? parseInt(imageId, 10) : null;\n      this.swipeEngine?.enable();\n      this.containerEl?.focus({ preventScroll: true });\n      await this.zoomEngine.focusImage(imageElement, bounds, originalLayout);\n      if (this.currentFocusIndex !== null) {\n        this.updateCounter(this.currentFocusIndex);\n      }\n      this.showNavButtons();\n      this.showFocusIndicator();\n    }\n  }\n\n  /**\n   * Clear the image cloud and reset state\n   */\n  clearImageCloud(): void {\n    // Clear queue processing interval to prevent stale images from being added\n    if (this.queueInterval !== null) {\n      clearInterval(this.queueInterval);\n      this.queueInterval = null;\n    }\n    // Increment generation to invalidate pending image onload handlers\n    this.loadGeneration++;\n    this.displayQueue = [];\n\n    this.hideFocusIndicator();\n\n    if (this.containerEl) {\n      this.containerEl.querySelectorAll('.fbn-ic-image, .fbn-ic-debug-center').forEach(el => el.remove());\n    }\n    this.imageElements = [];\n    this.imageLayouts = [];\n    this.currentFocusIndex = null;\n    this.hoveredImage = null;\n    this.layoutEngine.reset();\n    this.zoomEngine.reset();\n    this.idleAnimationEngine?.stopAll();\n    this.imagesLoaded = false;\n  }\n\n  private showLoading(show: boolean): void {\n    if (!this.fullConfig.ui.showLoadingSpinner || !this.loadingEl) return;\n    if (show) {\n      this.loadingEl.classList.remove('fbn-ic-hidden');\n    } else {\n      this.loadingEl.classList.add('fbn-ic-hidden');\n    }\n  }\n\n  private showError(message: string): void {\n    if (!this.errorEl) return;\n    this.errorEl.textContent = message;\n    this.errorEl.classList.remove('fbn-ic-hidden');\n  }\n\n  private hideError(): void {\n    if (this.errorEl) {\n      this.errorEl.classList.add('fbn-ic-hidden');\n    }\n  }\n\n  private updateCounter(index: number): void {\n    if (!this.fullConfig.ui.showImageCounter || !this.counterEl) return;\n    this.counterEl.textContent = `${index + 1} of ${this.imageElements.length}`;\n    this.counterEl.classList.remove('fbn-ic-hidden');\n  }\n\n  private hideCounter(): void {\n    if (this.counterEl) {\n      this.counterEl.classList.add('fbn-ic-hidden');\n    }\n  }\n\n  private showFocusIndicator(): void {\n    this.containerEl?.classList.add('fbn-ic-has-focus');\n  }\n\n  private hideFocusIndicator(): void {\n    this.containerEl?.classList.remove('fbn-ic-has-focus');\n  }\n\n  private showNavButtons(): void {\n    this.prevButtonEl?.classList.remove('fbn-ic-hidden');\n    this.nextButtonEl?.classList.remove('fbn-ic-hidden');\n  }\n\n  private hideNavButtons(): void {\n    this.prevButtonEl?.classList.add('fbn-ic-hidden');\n    this.nextButtonEl?.classList.add('fbn-ic-hidden');\n  }\n\n  /**\n   * Destroy the gallery and clean up resources\n   */\n  destroy(): void {\n    this.clearImageCloud();\n    // Remove auto-created UI elements\n    if (this.loadingElAutoCreated && this.loadingEl) {\n      this.loadingEl.remove();\n      this.loadingEl = null;\n    }\n    if (this.errorElAutoCreated && this.errorEl) {\n      this.errorEl.remove();\n      this.errorEl = null;\n    }\n    if (this.counterElAutoCreated && this.counterEl) {\n      this.counterEl.remove();\n      this.counterEl = null;\n    }\n    if (this.prevButtonElAutoCreated && this.prevButtonEl) {\n      this.prevButtonEl.remove();\n      this.prevButtonEl = null;\n    }\n    if (this.nextButtonElAutoCreated && this.nextButtonEl) {\n      this.nextButtonEl.remove();\n      this.nextButtonEl = null;\n    }\n    // Remove event listeners\n    if (this.resizeTimeout !== null) {\n      clearTimeout(this.resizeTimeout);\n    }\n    this.swipeEngine?.destroy();\n    this.idleAnimationEngine?.stopAll();\n    this.idleAnimationEngine = null;\n  }\n}\n","/**\n * Image Cloud Library - Main Entry Point\n *\n * Programmatic API for TypeScript/JavaScript applications\n */\n\n// Import CSS for bundlers that support it\nimport './styles/image-cloud.css';\n\n// Main class export\nexport { ImageCloud } from './ImageCloud';\n\n// Backwards compatibility alias\nexport { ImageCloud as ImageGallery } from './ImageCloud';\n\n// Factory function — recommended entry point for most users\nimport { ImageCloud as _ImageCloud } from './ImageCloud';\nimport type { ImageCloudOptions } from './config/types';\nexport async function imageCloud(options: ImageCloudOptions): Promise<_ImageCloud> {\n  const instance = new _ImageCloud(options);\n  await instance.init();\n  return instance;\n}\n\n// Type exports\nexport type {\n  ImageCloudOptions,\n  ImageGalleryOptions, // Backwards compatibility alias\n  ImageCloudConfig,\n  GalleryConfig,\n  AnimationConfig,\n  LayoutConfig,\n  LayoutAlgorithm,\n  GridAlgorithmConfig,\n  SpiralAlgorithmConfig,\n  ClusterAlgorithmConfig,\n  // Loader types (new)\n  LoaderEntry,\n  StaticLoaderEntry,\n  GoogleDriveLoaderEntry,\n  StaticLoaderInnerConfig,\n  GoogleDriveLoaderInnerConfig,\n  ConfigSection,\n  DebugConfig,\n  StaticUrlsSource,\n  StaticPathSource,\n  StaticJsonSource,\n  GoogleDriveFolderSource,\n  GoogleDriveFilesSource,\n  UIConfig,\n  ImageLayout,\n  ContainerBounds,\n  ResponsiveHeight,\n  StaticSource,\n  GoogleDriveSource,\n  PlacementLayout,\n  ImageLoader,\n  IImageFilter,\n  TransformParams,\n  // Entry path animation types\n  EntryPathType,\n  EntryPathConfig,\n  BouncePathConfig,\n  ElasticPathConfig,\n  WavePathConfig,\n  BouncePreset,\n  ElasticPreset,\n  WavePathPreset,\n  // Styling types\n  BorderStyle\n} from './config/types';\n\n// Default config export\nexport {\n  DEFAULT_CONFIG,\n  DEFAULT_SHARED_LOADER_CONFIG,\n  BOUNCE_PRESETS,\n  ELASTIC_PRESETS,\n  WAVE_PATH_PRESETS\n} from './config/defaults';\n\n// Export engines for advanced usage\nexport { AnimationEngine } from './engines/AnimationEngine';\nexport { LayoutEngine } from './engines/LayoutEngine';\nexport { ZoomEngine } from './engines/ZoomEngine';\nexport { EntryAnimationEngine } from './engines/EntryAnimationEngine';\nexport { animatePath, requiresJSAnimation } from './engines/PathAnimator';\n\n// Export layouts for custom implementations\nexport { RandomPlacementLayout } from './layouts/RandomPlacementLayout';\nexport { RadialPlacementLayout } from './layouts/RadialPlacementLayout';\nexport { GridPlacementLayout } from './layouts/GridPlacementLayout';\nexport { SpiralPlacementLayout } from './layouts/SpiralPlacementLayout';\nexport { ClusterPlacementLayout } from './layouts/ClusterPlacementLayout';\nexport { WavePlacementLayout } from './layouts/WavePlacementLayout';\nexport { HoneycombPlacementLayout } from './layouts/HoneycombPlacementLayout';\n\n// Export loaders for custom implementations\nexport { GoogleDriveLoader } from './loaders/GoogleDriveLoader';\nexport { StaticImageLoader } from './loaders/StaticImageLoader';\nexport { CompositeLoader } from './loaders/CompositeLoader';\nexport type { CompositeLoaderConfig } from './loaders/CompositeLoader';\nexport { ImageFilter } from './loaders/ImageFilter';\n\n// Export functional styles for manual injection if needed\nexport { injectFunctionalStyles, FUNCTIONAL_CSS } from './styles/functionalStyles';"]}