{"version":3,"file":"useCLSGuard.mjs","sources":["../../../../../src/lib/layouts/adaptive/hooks/useCLSGuard.ts"],"sourcesContent":["/**\n * @fileoverview useCLSGuard Hook\n *\n * Hook for preventing and monitoring Cumulative Layout Shift (CLS).\n * Provides layout reservations and real-time CLS measurement.\n *\n * @module layouts/adaptive/hooks/useCLSGuard\n * @version 1.0.0\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type {\n  CLSGuardConfig,\n  CLSMeasurement,\n  Dimensions,\n  LayoutReservation,\n  ReservationStrategy,\n  UseCLSGuardReturn,\n} from '../types';\nimport { DEFAULT_CLS_GUARD_CONFIG } from '../types';\nimport { createCLSGuard, type CLSGuard } from '../cls-guard';\n\n// =============================================================================\n// HOOK IMPLEMENTATION\n// =============================================================================\n\n/**\n * Options for useCLSGuard hook.\n */\nexport interface UseCLSGuardOptions {\n  /** CLS guard configuration */\n  config?: Partial<CLSGuardConfig>;\n  /** Callback when CLS threshold is exceeded */\n  onThresholdExceeded?: (score: number) => void;\n  /** Callback when CLS is measured */\n  onMeasurement?: (measurement: CLSMeasurement) => void;\n}\n\n/**\n * Hook for preventing and monitoring Cumulative Layout Shift.\n *\n * @param options - Hook configuration options\n * @returns CLS guard controls and metrics\n *\n * @example\n * ```tsx\n * function ImageGallery({ images }: { images: Image[] }) {\n *   const {\n *     clsScore,\n *     thresholdExceeded,\n *     reserve,\n *     release,\n *     reservations\n *   } = useCLSGuard({\n *     config: { maxCLS: 0.05 },\n *     onThresholdExceeded: (score) => {\n *       console.warn('CLS threshold exceeded:', score);\n *     }\n *   });\n *\n *   // Reserve space for images before they load\n *   useEffect(() => {\n *     for (const image of images) {\n *       reserve(image.id, { width: image.width, height: image.height });\n *     }\n *\n *     return () => {\n *       for (const image of images) {\n *         release(image.id);\n *       }\n *     };\n *   }, [images, reserve, release]);\n *\n *   return (\n *     <div>\n *       {thresholdExceeded && (\n *         <Warning>Layout shift detected! Score: {clsScore.toFixed(3)}</Warning>\n *       )}\n *       {images.map(image => (\n *         <img\n *           key={image.id}\n *           src={image.src}\n *           width={image.width}\n *           height={image.height}\n *           onLoad={() => release(image.id)}\n *         />\n *       ))}\n *     </div>\n *   );\n * }\n * ```\n */\nexport function useCLSGuard(options: UseCLSGuardOptions = {}): UseCLSGuardReturn {\n  const { config = {}, onThresholdExceeded, onMeasurement } = options;\n\n  // Refs\n  const guardRef = useRef<CLSGuard | null>(null);\n\n  // State\n  const [clsScore, setClsScore] = useState(0);\n  const [thresholdExceeded, setThresholdExceeded] = useState(false);\n  const [reservations, setReservations] = useState<ReadonlyMap<string, LayoutReservation>>(\n    new Map()\n  );\n  const [measurements, setMeasurements] = useState<readonly CLSMeasurement[]>([]);\n\n  // Merge config\n  const mergedConfig = useMemo<CLSGuardConfig>(\n    () => ({\n      ...DEFAULT_CLS_GUARD_CONFIG,\n      ...config,\n      onThresholdExceeded: (score) => {\n        setThresholdExceeded(true);\n        onThresholdExceeded?.(score);\n        config.onThresholdExceeded?.(score);\n      },\n    }),\n    [config, onThresholdExceeded]\n  );\n\n  // Initialize guard\n  useEffect(() => {\n    guardRef.current = createCLSGuard(mergedConfig) as CLSGuard;\n\n    // Subscribe to CLS updates\n    const unsubscribe = guardRef.current.observeCLS((measurement) => {\n      setClsScore(measurement.score);\n      setThresholdExceeded(measurement.thresholdExceeded);\n      setMeasurements((prev) => [...prev, measurement]);\n      onMeasurement?.(measurement);\n    });\n\n    return () => {\n      unsubscribe();\n      guardRef.current?.destroy();\n      guardRef.current = null;\n    };\n  }, [mergedConfig, onMeasurement]);\n\n  // Create reservation\n  const reserve = useCallback(\n    (id: string, dimensions: Dimensions, strategy?: ReservationStrategy): LayoutReservation => {\n      const guard = guardRef.current;\n      if (!guard) {\n        // Return a stub reservation\n        return {\n          id,\n          dimensions,\n          active: false,\n          createdAt: Date.now(),\n        };\n      }\n\n      const reservation = guard.createReservation(id, dimensions, strategy);\n\n      setReservations((prev) => {\n        const next = new Map(prev);\n        next.set(id, reservation);\n        return next;\n      });\n\n      return reservation;\n    },\n    []\n  );\n\n  // Release reservation\n  const release = useCallback((id: string) => {\n    const guard = guardRef.current;\n    if (guard) {\n      guard.releaseReservation(id);\n    }\n\n    setReservations((prev) => {\n      const next = new Map(prev);\n      next.delete(id);\n      return next;\n    });\n  }, []);\n\n  return {\n    clsScore,\n    thresholdExceeded,\n    reserve,\n    release,\n    reservations,\n    measurements,\n  };\n}\n\n// =============================================================================\n// HELPER HOOKS\n// =============================================================================\n\n/**\n * Hook for reserving layout space for an image.\n *\n * @param id - Unique identifier for the image\n * @param src - Image source URL\n * @param dimensions - Known dimensions (if available)\n * @returns Loading state and reservation info\n *\n * @example\n * ```tsx\n * function ReservedImage({ id, src, width, height }: ImageProps) {\n *   const { isLoading, reservation } = useImageReservation(id, src, { width, height });\n *\n *   return (\n *     <div style={{ width, height }}>\n *       {isLoading && <Skeleton />}\n *       <img\n *         src={src}\n *         width={width}\n *         height={height}\n *         style={{ opacity: isLoading ? 0 : 1 }}\n *       />\n *     </div>\n *   );\n * }\n * ```\n */\nexport function useImageReservation(\n  id: string,\n  src: string,\n  dimensions?: Dimensions\n): {\n  isLoading: boolean;\n  reservation: LayoutReservation | null;\n  intrinsicDimensions: Dimensions | null;\n} {\n  const [isLoading, setIsLoading] = useState(true);\n  const [reservation, setReservation] = useState<LayoutReservation | null>(null);\n  const [intrinsicDimensions, setIntrinsicDimensions] = useState<Dimensions | null>(null);\n\n  const { reserve, release } = useCLSGuard();\n\n  useEffect(() => {\n    // Use requestAnimationFrame to defer state update\n    const frameId = requestAnimationFrame(() => {\n      setIsLoading(true);\n    });\n\n    // If dimensions are provided, create reservation immediately\n    if (dimensions != null) {\n      const res = reserve(id, dimensions);\n      requestAnimationFrame(() => {\n        setReservation(res);\n      });\n    }\n\n    // Return cleanup for the frame\n    const cleanupFrame = (): void => {\n      cancelAnimationFrame(frameId);\n    };\n\n    // Load image to get intrinsic dimensions\n    const img = new Image();\n\n    img.onload = () => {\n      const intrinsic = { width: img.naturalWidth, height: img.naturalHeight };\n      setIntrinsicDimensions(intrinsic);\n\n      // Update reservation if we didn't have dimensions\n      if (dimensions == null) {\n        const res = reserve(id, intrinsic);\n        setReservation(res);\n      }\n\n      setIsLoading(false);\n    };\n\n    img.onerror = () => {\n      setIsLoading(false);\n    };\n\n    img.src = src;\n\n    return () => {\n      cleanupFrame();\n      release(id);\n    };\n  }, [id, src, dimensions, reserve, release]);\n\n  return {\n    isLoading,\n    reservation,\n    intrinsicDimensions,\n  };\n}\n\n/**\n * Hook for monitoring CLS and triggering optimization actions.\n *\n * @param threshold - CLS threshold to monitor\n * @param onExceeded - Callback when threshold is exceeded\n *\n * @example\n * ```tsx\n * function PerformanceMonitor() {\n *   useCLSMonitor(0.1, () => {\n *     // Disable animations to prevent further shifts\n *     document.body.classList.add('reduce-motion');\n *   });\n *\n *   return null;\n * }\n * ```\n */\nexport function useCLSMonitor(threshold: number, onExceeded: (score: number) => void): void {\n  const { clsScore, thresholdExceeded } = useCLSGuard({\n    config: { maxCLS: threshold },\n  });\n\n  const hasNotifiedRef = useRef(false);\n\n  useEffect(() => {\n    if (thresholdExceeded && !hasNotifiedRef.current) {\n      hasNotifiedRef.current = true;\n      onExceeded(clsScore);\n    }\n  }, [thresholdExceeded, clsScore, onExceeded]);\n}\n\n// =============================================================================\n// EXPORTS\n// =============================================================================\n\nexport type { UseCLSGuardReturn };\n"],"names":["useCLSGuard","options","config","onThresholdExceeded","onMeasurement","guardRef","useRef","clsScore","setClsScore","useState","thresholdExceeded","setThresholdExceeded","reservations","setReservations","measurements","setMeasurements","mergedConfig","useMemo","DEFAULT_CLS_GUARD_CONFIG","score","useEffect","createCLSGuard","unsubscribe","measurement","prev","reserve","useCallback","id","dimensions","strategy","guard","reservation","next","release","useImageReservation","src","isLoading","setIsLoading","setReservation","intrinsicDimensions","setIntrinsicDimensions","frameId","res","cleanupFrame","img","intrinsic","useCLSMonitor","threshold","onExceeded","hasNotifiedRef"],"mappings":"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"}