{
  "name": "cropper",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "@diceui/use-as-ref",
    "@diceui/use-isomorphic-layout-effect",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/cropper.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\n\nconst ROOT_NAME = \"Cropper\";\nconst ROOT_IMPL_NAME = \"CropperImpl\";\nconst IMAGE_NAME = \"CropperImage\";\nconst VIDEO_NAME = \"CropperVideo\";\nconst AREA_NAME = \"CropperArea\";\n\ninterface Point {\n  x: number;\n  y: number;\n}\n\ninterface GestureEvent extends UIEvent {\n  rotation: number;\n  scale: number;\n  clientX: number;\n  clientY: number;\n}\n\ninterface Size {\n  width: number;\n  height: number;\n}\n\ninterface Area {\n  width: number;\n  height: number;\n  x: number;\n  y: number;\n}\n\ninterface MediaSize {\n  width: number;\n  height: number;\n  naturalWidth: number;\n  naturalHeight: number;\n}\n\ntype Shape = \"rectangle\" | \"circle\";\ntype ObjectFit = \"contain\" | \"cover\" | \"horizontal-cover\" | \"vertical-cover\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\nconst MAX_CACHE_SIZE = 200;\nconst DPR = typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1;\n\nconst rotationSizeCache = new Map<string, Size>();\nconst cropSizeCache = new Map<string, Size>();\nconst croppedAreaCache = new Map<\n  string,\n  { croppedAreaPercentages: Area; croppedAreaPixels: Area }\n>();\nconst onPositionClampCache = new Map<string, Point>();\n\nfunction clamp(value: number, min: number, max: number): number {\n  return Math.min(Math.max(value, min), max);\n}\n\nfunction quantize(n: number, step = 2 / DPR): number {\n  return Math.round(n / step) * step;\n}\n\nfunction quantizePosition(n: number, step = 4 / DPR): number {\n  return Math.round(n / step) * step;\n}\n\nfunction quantizeZoom(n: number, step = 0.01): number {\n  return Math.round(n / step) * step;\n}\n\nfunction quantizeRotation(n: number, step = 1.0): number {\n  return Math.round(n / step) * step;\n}\n\nfunction snapToDevicePixel(n: number): number {\n  return Math.round(n * DPR) / DPR;\n}\n\nfunction lruGet<K, V>(map: Map<K, V>, key: K): V | undefined {\n  const v = map.get(key);\n  if (v !== undefined) {\n    map.delete(key);\n    map.set(key, v);\n  }\n  return v;\n}\n\nfunction lruSet<K, V>(\n  map: Map<K, V>,\n  key: K,\n  val: V,\n  max = MAX_CACHE_SIZE,\n): void {\n  if (map.has(key)) {\n    map.delete(key);\n  }\n  map.set(key, val);\n  if (map.size > max) {\n    const firstKey = map.keys().next().value;\n    if (firstKey !== undefined) {\n      map.delete(firstKey);\n    }\n  }\n}\n\nfunction getDistanceBetweenPoints(pointA: Point, pointB: Point): number {\n  return Math.sqrt((pointA.y - pointB.y) ** 2 + (pointA.x - pointB.x) ** 2);\n}\n\nfunction getCenter(a: Point, b: Point): Point {\n  return {\n    x: (b.x + a.x) * 0.5,\n    y: (b.y + a.y) * 0.5,\n  };\n}\n\nfunction getRotationBetweenPoints(pointA: Point, pointB: Point): number {\n  return (Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180) / Math.PI;\n}\n\nfunction getRadianAngle(degreeValue: number): number {\n  return (degreeValue * Math.PI) / 180;\n}\n\nfunction rotateSize(width: number, height: number, rotation: number): Size {\n  const cacheKey = `${quantize(width)}-${quantize(height)}-${quantizeRotation(rotation)}`;\n\n  const cached = lruGet(rotationSizeCache, cacheKey);\n  if (cached) {\n    return cached;\n  }\n  const rotRad = getRadianAngle(rotation);\n  const cosRot = Math.cos(rotRad);\n  const sinRot = Math.sin(rotRad);\n\n  const result: Size = {\n    width: Math.abs(cosRot * width) + Math.abs(sinRot * height),\n    height: Math.abs(sinRot * width) + Math.abs(cosRot * height),\n  };\n\n  lruSet(rotationSizeCache, cacheKey, result, MAX_CACHE_SIZE);\n  return result;\n}\n\nfunction getCropSize(\n  mediaWidth: number,\n  mediaHeight: number,\n  contentWidth: number,\n  contentHeight: number,\n  aspect: number,\n  rotation = 0,\n): Size {\n  const cacheKey = `${quantize(mediaWidth, 8)}-${quantize(mediaHeight, 8)}-${quantize(contentWidth, 8)}-${quantize(contentHeight, 8)}-${quantize(aspect, 0.01)}-${quantizeRotation(rotation)}`;\n\n  const cached = lruGet(cropSizeCache, cacheKey);\n  if (cached) {\n    return cached;\n  }\n  const { width, height } = rotateSize(mediaWidth, mediaHeight, rotation);\n  const fittingWidth = Math.min(width, contentWidth);\n  const fittingHeight = Math.min(height, contentHeight);\n\n  const result: Size =\n    fittingWidth > fittingHeight * aspect\n      ? {\n          width: fittingHeight * aspect,\n          height: fittingHeight,\n        }\n      : {\n          width: fittingWidth,\n          height: fittingWidth / aspect,\n        };\n\n  lruSet(cropSizeCache, cacheKey, result, MAX_CACHE_SIZE);\n  return result;\n}\n\nfunction onPositionClamp(\n  position: Point,\n  mediaSize: Size,\n  cropSize: Size,\n  zoom: number,\n  rotation = 0,\n): Point {\n  const quantizedX = quantizePosition(position.x);\n  const quantizedY = quantizePosition(position.y);\n\n  const cacheKey = `${quantizedX}-${quantizedY}-${quantize(mediaSize.width)}-${quantize(mediaSize.height)}-${quantize(cropSize.width)}-${quantize(cropSize.height)}-${quantizeZoom(zoom)}-${quantizeRotation(rotation)}`;\n\n  const cached = lruGet(onPositionClampCache, cacheKey);\n  if (cached) {\n    return cached;\n  }\n  const { width, height } = rotateSize(\n    mediaSize.width,\n    mediaSize.height,\n    rotation,\n  );\n\n  const maxPositionX = width * zoom * 0.5 - cropSize.width * 0.5;\n  const maxPositionY = height * zoom * 0.5 - cropSize.height * 0.5;\n\n  const result: Point = {\n    x: clamp(position.x, -maxPositionX, maxPositionX),\n    y: clamp(position.y, -maxPositionY, maxPositionY),\n  };\n\n  lruSet(onPositionClampCache, cacheKey, result, MAX_CACHE_SIZE);\n  return result;\n}\n\nfunction getCroppedArea(\n  crop: Point,\n  mediaSize: MediaSize,\n  cropSize: Size,\n  aspect: number,\n  zoom: number,\n  rotation = 0,\n  allowOverflow = false,\n): { croppedAreaPercentages: Area; croppedAreaPixels: Area } {\n  const cacheKey = `${quantizePosition(crop.x)}-${quantizePosition(crop.y)}-${quantize(mediaSize.width)}-${quantize(mediaSize.height)}-${quantize(mediaSize.naturalWidth)}-${quantize(mediaSize.naturalHeight)}-${quantize(cropSize.width)}-${quantize(cropSize.height)}-${quantize(aspect, 0.01)}-${quantizeZoom(zoom)}-${quantizeRotation(rotation)}-${allowOverflow}`;\n\n  const cached = lruGet(croppedAreaCache, cacheKey);\n\n  if (cached) return cached;\n\n  const onAreaLimit = !allowOverflow\n    ? (max: number, value: number) => Math.min(max, Math.max(0, value))\n    : (_max: number, value: number) => value;\n\n  const mediaBBoxSize = rotateSize(mediaSize.width, mediaSize.height, rotation);\n  const mediaNaturalBBoxSize = rotateSize(\n    mediaSize.naturalWidth,\n    mediaSize.naturalHeight,\n    rotation,\n  );\n\n  const croppedAreaPercentages: Area = {\n    x: onAreaLimit(\n      100,\n      (((mediaBBoxSize.width - cropSize.width / zoom) / 2 - crop.x / zoom) /\n        mediaBBoxSize.width) *\n        100,\n    ),\n    y: onAreaLimit(\n      100,\n      (((mediaBBoxSize.height - cropSize.height / zoom) / 2 - crop.y / zoom) /\n        mediaBBoxSize.height) *\n        100,\n    ),\n    width: onAreaLimit(\n      100,\n      ((cropSize.width / mediaBBoxSize.width) * 100) / zoom,\n    ),\n    height: onAreaLimit(\n      100,\n      ((cropSize.height / mediaBBoxSize.height) * 100) / zoom,\n    ),\n  };\n\n  const widthInPixels = Math.round(\n    onAreaLimit(\n      mediaNaturalBBoxSize.width,\n      (croppedAreaPercentages.width * mediaNaturalBBoxSize.width) / 100,\n    ),\n  );\n  const heightInPixels = Math.round(\n    onAreaLimit(\n      mediaNaturalBBoxSize.height,\n      (croppedAreaPercentages.height * mediaNaturalBBoxSize.height) / 100,\n    ),\n  );\n  const isImageWiderThanHigh =\n    mediaNaturalBBoxSize.width >= mediaNaturalBBoxSize.height * aspect;\n\n  const sizePixels: Size = isImageWiderThanHigh\n    ? {\n        width: Math.round(heightInPixels * aspect),\n        height: heightInPixels,\n      }\n    : {\n        width: widthInPixels,\n        height: Math.round(widthInPixels / aspect),\n      };\n\n  const croppedAreaPixels: Area = {\n    ...sizePixels,\n    x: Math.round(\n      onAreaLimit(\n        mediaNaturalBBoxSize.width - sizePixels.width,\n        (croppedAreaPercentages.x * mediaNaturalBBoxSize.width) / 100,\n      ),\n    ),\n    y: Math.round(\n      onAreaLimit(\n        mediaNaturalBBoxSize.height - sizePixels.height,\n        (croppedAreaPercentages.y * mediaNaturalBBoxSize.height) / 100,\n      ),\n    ),\n  };\n\n  const result = { croppedAreaPercentages, croppedAreaPixels };\n\n  lruSet(croppedAreaCache, cacheKey, result, MAX_CACHE_SIZE);\n  return result;\n}\n\ninterface StoreState {\n  crop: Point;\n  zoom: number;\n  rotation: number;\n  mediaSize: MediaSize | null;\n  cropSize: Size | null;\n  isDragging: boolean;\n  isWheelZooming: boolean;\n}\n\ninterface Store {\n  subscribe: (callback: () => void) => () => void;\n  getState: () => StoreState;\n  setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;\n  notify: () => void;\n  batch: (fn: () => void) => void;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n  const context = React.useContext(StoreContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\nfunction useStore<T>(selector: (state: StoreState) => T): T {\n  const store = useStoreContext(\"useStore\");\n\n  const getSnapshot = React.useCallback(\n    () => selector(store.getState()),\n    [store, selector],\n  );\n\n  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ntype RootElement = React.ComponentRef<typeof CropperImpl>;\n\ninterface CropperContextValue {\n  aspectRatio: number;\n  minZoom: number;\n  maxZoom: number;\n  zoomSpeed: number;\n  keyboardStep: number;\n  shape: Shape;\n  objectFit: ObjectFit;\n  rootRef: React.RefObject<RootElement | null>;\n  allowOverflow: boolean;\n  preventScrollZoom: boolean;\n  withGrid: boolean;\n}\n\nconst CropperContext = React.createContext<CropperContextValue | null>(null);\n\nfunction useCropperContext(consumerName: string) {\n  const context = React.useContext(CropperContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface CropperProps extends DivProps {\n  crop?: Point;\n  zoom?: number;\n  minZoom?: number;\n  maxZoom?: number;\n  zoomSpeed?: number;\n  rotation?: number;\n  keyboardStep?: number;\n  aspectRatio?: number;\n  shape?: Shape;\n  objectFit?: ObjectFit;\n  allowOverflow?: boolean;\n  preventScrollZoom?: boolean;\n  withGrid?: boolean;\n  onCropChange?: (crop: Point) => void;\n  onCropSizeChange?: (cropSize: Size) => void;\n  onCropAreaChange?: (croppedArea: Area, croppedAreaPixels: Area) => void;\n  onCropComplete?: (croppedArea: Area, croppedAreaPixels: Area) => void;\n  onZoomChange?: (zoom: number) => void;\n  onRotationChange?: (rotation: number) => void;\n  onMediaLoaded?: (mediaSize: MediaSize) => void;\n  onInteractionStart?: () => void;\n  onInteractionEnd?: () => void;\n  onWheelZoom?: (event: WheelEvent) => void;\n}\n\nfunction Cropper(props: CropperProps) {\n  const {\n    crop = { x: 0, y: 0 },\n    zoom = 1,\n    minZoom = 1,\n    maxZoom = 3,\n    zoomSpeed = 1,\n    rotation = 0,\n    keyboardStep = 1,\n    aspectRatio = 4 / 3,\n    shape = \"rectangle\",\n    objectFit = \"contain\",\n    allowOverflow = false,\n    preventScrollZoom = false,\n    withGrid = false,\n    onCropChange,\n    onCropSizeChange,\n    onCropAreaChange,\n    onCropComplete,\n    onZoomChange,\n    onRotationChange,\n    onMediaLoaded,\n    onInteractionStart,\n    onInteractionEnd,\n    className,\n    ...rootProps\n  } = props;\n\n  const listenersRef = useLazyRef(() => new Set<() => void>());\n  const stateRef = useLazyRef<StoreState>(() => ({\n    crop,\n    zoom,\n    rotation,\n    mediaSize: null,\n    cropSize: null,\n    isDragging: false,\n    isWheelZooming: false,\n  }));\n\n  const propsRef = useAsRef({\n    onCropChange,\n    onCropSizeChange,\n    onCropAreaChange,\n    onCropComplete,\n    onZoomChange,\n    onRotationChange,\n    onMediaLoaded,\n    onInteractionStart,\n    onInteractionEnd,\n  });\n\n  const rootRef = React.useRef<RootElement | null>(null);\n\n  const store = React.useMemo<Store>(() => {\n    let isBatching = false;\n    let raf: number | null = null;\n\n    function notifyCropAreaChange() {\n      if (raf != null) return;\n      raf = requestAnimationFrame(() => {\n        raf = null;\n        const s = stateRef.current;\n        if (s?.mediaSize && s.cropSize && propsRef.current.onCropAreaChange) {\n          const { croppedAreaPercentages, croppedAreaPixels } = getCroppedArea(\n            s.crop,\n            s.mediaSize,\n            s.cropSize,\n            aspectRatio,\n            s.zoom,\n            s.rotation,\n          );\n          propsRef.current.onCropAreaChange(\n            croppedAreaPercentages,\n            croppedAreaPixels,\n          );\n        }\n      });\n    }\n\n    return {\n      subscribe: (cb) => {\n        listenersRef.current.add(cb);\n        return () => listenersRef.current.delete(cb);\n      },\n      getState: () => stateRef.current,\n      setState: (key, value) => {\n        if (Object.is(stateRef.current[key], value)) return;\n\n        stateRef.current[key] = value;\n\n        if (\n          key === \"crop\" &&\n          typeof value === \"object\" &&\n          value &&\n          \"x\" in value\n        ) {\n          propsRef.current.onCropChange?.(value);\n        } else if (key === \"zoom\" && typeof value === \"number\") {\n          propsRef.current.onZoomChange?.(value);\n        } else if (key === \"rotation\" && typeof value === \"number\") {\n          propsRef.current.onRotationChange?.(value);\n        } else if (\n          key === \"cropSize\" &&\n          typeof value === \"object\" &&\n          value &&\n          \"width\" in value\n        ) {\n          propsRef.current.onCropSizeChange?.(value);\n        } else if (\n          key === \"mediaSize\" &&\n          typeof value === \"object\" &&\n          value &&\n          \"naturalWidth\" in value\n        ) {\n          propsRef.current.onMediaLoaded?.(value);\n        } else if (key === \"isDragging\") {\n          if (value) {\n            propsRef.current.onInteractionStart?.();\n          } else {\n            propsRef.current.onInteractionEnd?.();\n            const currentState = stateRef.current;\n            if (\n              currentState?.mediaSize &&\n              currentState.cropSize &&\n              propsRef.current.onCropComplete\n            ) {\n              const { croppedAreaPercentages, croppedAreaPixels } =\n                getCroppedArea(\n                  currentState.crop,\n                  currentState.mediaSize,\n                  currentState.cropSize,\n                  aspectRatio,\n                  currentState.zoom,\n                  currentState.rotation,\n                );\n              propsRef.current.onCropComplete(\n                croppedAreaPercentages,\n                croppedAreaPixels,\n              );\n            }\n          }\n        }\n\n        if (\n          (key === \"crop\" ||\n            key === \"zoom\" ||\n            key === \"rotation\" ||\n            key === \"mediaSize\" ||\n            key === \"cropSize\") &&\n          propsRef.current.onCropAreaChange\n        ) {\n          notifyCropAreaChange();\n        }\n\n        if (!isBatching) {\n          store.notify();\n        }\n      },\n      notify: () => {\n        for (const cb of listenersRef.current) {\n          cb();\n        }\n      },\n      batch: (fn: () => void) => {\n        if (isBatching) {\n          fn();\n          return;\n        }\n        isBatching = true;\n        try {\n          fn();\n        } finally {\n          isBatching = false;\n          store.notify();\n        }\n      },\n    };\n  }, [listenersRef, stateRef, propsRef, aspectRatio]);\n\n  useIsomorphicLayoutEffect(() => {\n    const updates: Partial<StoreState> = {};\n    let hasUpdates = false;\n    let shouldRecompute = false;\n\n    if (crop !== undefined) {\n      const currentState = store.getState();\n      if (!Object.is(currentState.crop, crop)) {\n        updates.crop = crop;\n        hasUpdates = true;\n      }\n    }\n\n    if (zoom !== undefined) {\n      const currentState = store.getState();\n      if (currentState.zoom !== zoom) {\n        updates.zoom = zoom;\n        hasUpdates = true;\n        shouldRecompute = true;\n      }\n    }\n\n    if (rotation !== undefined) {\n      const currentState = store.getState();\n      if (currentState.rotation !== rotation) {\n        updates.rotation = rotation;\n        hasUpdates = true;\n        shouldRecompute = true;\n      }\n    }\n\n    if (hasUpdates) {\n      store.batch(() => {\n        Object.entries(updates).forEach(([key, value]) => {\n          store.setState(key as keyof StoreState, value);\n        });\n      });\n\n      if (shouldRecompute && rootRef.current) {\n        requestAnimationFrame(() => {\n          const currentState = store.getState();\n          if (currentState.cropSize && currentState.mediaSize) {\n            const newPosition = !allowOverflow\n              ? onPositionClamp(\n                  currentState.crop,\n                  currentState.mediaSize,\n                  currentState.cropSize,\n                  currentState.zoom,\n                  currentState.rotation,\n                )\n              : currentState.crop;\n\n            if (\n              Math.abs(newPosition.x - currentState.crop.x) > 0.001 ||\n              Math.abs(newPosition.y - currentState.crop.y) > 0.001\n            ) {\n              store.setState(\"crop\", newPosition);\n            }\n          }\n        });\n      }\n    }\n  }, [crop, zoom, rotation, store, allowOverflow]);\n\n  const contextValue = React.useMemo<CropperContextValue>(\n    () => ({\n      minZoom,\n      maxZoom,\n      zoomSpeed,\n      keyboardStep,\n      aspectRatio,\n      shape,\n      objectFit,\n      preventScrollZoom,\n      allowOverflow,\n      withGrid,\n      rootRef,\n    }),\n    [\n      minZoom,\n      maxZoom,\n      zoomSpeed,\n      keyboardStep,\n      aspectRatio,\n      shape,\n      objectFit,\n      preventScrollZoom,\n      allowOverflow,\n      withGrid,\n    ],\n  );\n\n  return (\n    <StoreContext.Provider value={store}>\n      <CropperContext.Provider value={contextValue}>\n        <div\n          data-slot=\"cropper-wrapper\"\n          className={cn(\"relative size-full overflow-hidden\", className)}\n        >\n          <CropperImpl {...rootProps} />\n        </div>\n      </CropperContext.Provider>\n    </StoreContext.Provider>\n  );\n}\n\ninterface CropperImplProps extends CropperProps {\n  onWheelZoom?: (event: WheelEvent) => void;\n}\n\nfunction CropperImpl(props: CropperImplProps) {\n  const {\n    onWheelZoom: onWheelZoomProp,\n    onKeyUp: onKeyUpProp,\n    onKeyDown: onKeyDownProp,\n    onMouseDown: onMouseDownProp,\n    onTouchStart: onTouchStartProp,\n    asChild,\n    className,\n    ref,\n    ...rootImplProps\n  } = props;\n\n  const context = useCropperContext(ROOT_IMPL_NAME);\n  const store = useStoreContext(ROOT_IMPL_NAME);\n  const crop = useStore((state) => state.crop);\n  const zoom = useStore((state) => state.zoom);\n  const rotation = useStore((state) => state.rotation);\n  const mediaSize = useStore((state) => state.mediaSize);\n  const cropSize = useStore((state) => state.cropSize);\n\n  const propsRef = useAsRef({\n    onWheelZoom: onWheelZoomProp,\n    onKeyUp: onKeyUpProp,\n    onKeyDown: onKeyDownProp,\n    onMouseDown: onMouseDownProp,\n    onTouchStart: onTouchStartProp,\n  });\n\n  const composedRef = useComposedRefs(ref, context.rootRef);\n  const dragStartPositionRef = React.useRef<Point>({ x: 0, y: 0 });\n  const dragStartCropRef = React.useRef<Point>({ x: 0, y: 0 });\n  const contentPositionRef = React.useRef<Point>({ x: 0, y: 0 });\n  const lastPinchDistanceRef = React.useRef(0);\n  const lastPinchRotationRef = React.useRef(0);\n  const rafDragTimeoutRef = React.useRef<number | null>(null);\n  const rafPinchTimeoutRef = React.useRef<number | null>(null);\n  const wheelTimerRef = React.useRef<number | null>(null);\n  const isTouchingRef = React.useRef(false);\n  const gestureZoomStartRef = React.useRef(0);\n  const gestureRotationStartRef = React.useRef(0);\n\n  const onRefsCleanup = React.useCallback(() => {\n    if (rafDragTimeoutRef.current) {\n      cancelAnimationFrame(rafDragTimeoutRef.current);\n      rafDragTimeoutRef.current = null;\n    }\n    if (rafPinchTimeoutRef.current) {\n      cancelAnimationFrame(rafPinchTimeoutRef.current);\n      rafPinchTimeoutRef.current = null;\n    }\n    if (wheelTimerRef.current) {\n      clearTimeout(wheelTimerRef.current);\n      wheelTimerRef.current = null;\n    }\n    isTouchingRef.current = false;\n  }, []);\n\n  const onCacheCleanup = React.useCallback(() => {\n    if (onPositionClampCache.size > MAX_CACHE_SIZE * 1.5) {\n      onPositionClampCache.clear();\n    }\n    if (croppedAreaCache.size > MAX_CACHE_SIZE * 1.5) {\n      croppedAreaCache.clear();\n    }\n  }, []);\n\n  const getMousePoint = React.useCallback(\n    (event: MouseEvent | React.MouseEvent) => ({\n      x: Number(event.clientX),\n      y: Number(event.clientY),\n    }),\n    [],\n  );\n\n  const getTouchPoint = React.useCallback(\n    (touch: Touch | React.Touch) => ({\n      x: Number(touch.clientX),\n      y: Number(touch.clientY),\n    }),\n    [],\n  );\n\n  const onContentPositionChange = React.useCallback(() => {\n    if (context.rootRef?.current) {\n      const bounds = context.rootRef.current.getBoundingClientRect();\n      contentPositionRef.current = { x: bounds.left, y: bounds.top };\n    }\n  }, [context.rootRef]);\n\n  const getPointOnContent = React.useCallback(\n    ({ x, y }: Point, contentTopLeft: Point): Point => {\n      if (!context.rootRef?.current) {\n        return { x: 0, y: 0 };\n      }\n      const contentRect = context.rootRef.current.getBoundingClientRect();\n      return {\n        x: contentRect.width / 2 - (x - contentTopLeft.x),\n        y: contentRect.height / 2 - (y - contentTopLeft.y),\n      };\n    },\n    [context.rootRef],\n  );\n\n  const getPointOnMedia = React.useCallback(\n    ({ x, y }: Point) => {\n      return {\n        x: (x + crop.x) / zoom,\n        y: (y + crop.y) / zoom,\n      };\n    },\n    [crop, zoom],\n  );\n\n  const recomputeCropPosition = React.useCallback(() => {\n    if (!cropSize || !mediaSize) return;\n\n    const newPosition = !context.allowOverflow\n      ? onPositionClamp(crop, mediaSize, cropSize, zoom, rotation)\n      : crop;\n\n    if (\n      Math.abs(newPosition.x - crop.x) > 0.001 ||\n      Math.abs(newPosition.y - crop.y) > 0.001\n    ) {\n      store.setState(\"crop\", newPosition);\n    }\n  }, [cropSize, mediaSize, context.allowOverflow, crop, zoom, rotation, store]);\n\n  const onZoomChange = React.useCallback(\n    (newZoom: number, point: Point, shouldUpdatePosition = true) => {\n      if (!cropSize || !mediaSize) return;\n\n      const clampedZoom = clamp(newZoom, context.minZoom, context.maxZoom);\n\n      store.batch(() => {\n        if (shouldUpdatePosition) {\n          const zoomPoint = getPointOnContent(\n            point,\n            contentPositionRef.current,\n          );\n          const zoomTarget = getPointOnMedia(zoomPoint);\n          const requestedPosition = {\n            x: zoomTarget.x * clampedZoom - zoomPoint.x,\n            y: zoomTarget.y * clampedZoom - zoomPoint.y,\n          };\n\n          const newPosition = !context.allowOverflow\n            ? onPositionClamp(\n                requestedPosition,\n                mediaSize,\n                cropSize,\n                clampedZoom,\n                rotation,\n              )\n            : requestedPosition;\n\n          store.setState(\"crop\", newPosition);\n        }\n        store.setState(\"zoom\", clampedZoom);\n      });\n\n      requestAnimationFrame(() => {\n        recomputeCropPosition();\n      });\n    },\n    [\n      cropSize,\n      mediaSize,\n      context.minZoom,\n      context.maxZoom,\n      context.allowOverflow,\n      getPointOnContent,\n      getPointOnMedia,\n      rotation,\n      store,\n      recomputeCropPosition,\n    ],\n  );\n\n  const onDragStart = React.useCallback(\n    ({ x, y }: Point) => {\n      dragStartPositionRef.current = { x, y };\n      dragStartCropRef.current = { ...crop };\n      store.setState(\"isDragging\", true);\n    },\n    [crop, store],\n  );\n\n  const onDrag = React.useCallback(\n    ({ x, y }: Point) => {\n      if (rafDragTimeoutRef.current) {\n        cancelAnimationFrame(rafDragTimeoutRef.current);\n      }\n\n      rafDragTimeoutRef.current = requestAnimationFrame(() => {\n        if (!cropSize || !mediaSize) return;\n        if (x === undefined || y === undefined) return;\n\n        const offsetX = x - dragStartPositionRef.current.x;\n        const offsetY = y - dragStartPositionRef.current.y;\n\n        if (Math.abs(offsetX) < 2 && Math.abs(offsetY) < 2) {\n          return;\n        }\n\n        const requestedPosition = {\n          x: dragStartCropRef.current.x + offsetX,\n          y: dragStartCropRef.current.y + offsetY,\n        };\n\n        const newPosition = !context.allowOverflow\n          ? onPositionClamp(\n              requestedPosition,\n              mediaSize,\n              cropSize,\n              zoom,\n              rotation,\n            )\n          : requestedPosition;\n\n        const currentCrop = store.getState().crop;\n        if (\n          Math.abs(newPosition.x - currentCrop.x) > 1 ||\n          Math.abs(newPosition.y - currentCrop.y) > 1\n        ) {\n          store.setState(\"crop\", newPosition);\n        }\n      });\n    },\n    [cropSize, mediaSize, context.allowOverflow, zoom, rotation, store],\n  );\n\n  const onMouseMove = React.useCallback(\n    (event: MouseEvent) => onDrag(getMousePoint(event)),\n    [getMousePoint, onDrag],\n  );\n\n  const onTouchMove = React.useCallback(\n    (event: TouchEvent) => {\n      event.preventDefault();\n      if (event.touches.length === 2) {\n        const [firstTouch, secondTouch] = event.touches ?? [];\n        if (firstTouch && secondTouch) {\n          const pointA = getTouchPoint(firstTouch);\n          const pointB = getTouchPoint(secondTouch);\n          const center = getCenter(pointA, pointB);\n          onDrag(center);\n\n          if (rafPinchTimeoutRef.current) {\n            cancelAnimationFrame(rafPinchTimeoutRef.current);\n          }\n\n          rafPinchTimeoutRef.current = requestAnimationFrame(() => {\n            const distance = getDistanceBetweenPoints(pointA, pointB);\n            const distanceRatio = distance / lastPinchDistanceRef.current;\n\n            if (Math.abs(distanceRatio - 1) > 0.01) {\n              const newZoom = zoom * distanceRatio;\n              onZoomChange(newZoom, center, false);\n              lastPinchDistanceRef.current = distance;\n            }\n\n            const rotationAngle = getRotationBetweenPoints(pointA, pointB);\n            const rotationDiff = rotationAngle - lastPinchRotationRef.current;\n\n            if (Math.abs(rotationDiff) > 0.5) {\n              const newRotation = rotation + rotationDiff;\n              store.setState(\"rotation\", newRotation);\n              lastPinchRotationRef.current = rotationAngle;\n            }\n          });\n        }\n      } else if (event.touches.length === 1) {\n        const firstTouch = event.touches[0];\n        if (firstTouch) {\n          onDrag(getTouchPoint(firstTouch));\n        }\n      }\n    },\n    [getTouchPoint, onDrag, zoom, onZoomChange, rotation, store],\n  );\n\n  const onGestureChange = React.useCallback(\n    (event: GestureEvent) => {\n      event.preventDefault();\n      if (isTouchingRef.current) {\n        return;\n      }\n\n      const point = { x: Number(event.clientX), y: Number(event.clientY) };\n      const newZoom = gestureZoomStartRef.current - 1 + event.scale;\n      onZoomChange(newZoom, point, true);\n\n      const newRotation = gestureRotationStartRef.current + event.rotation;\n      store.setState(\"rotation\", newRotation);\n    },\n    [onZoomChange, store],\n  );\n\n  const onGestureEnd = React.useCallback(() => {\n    document.removeEventListener(\n      \"gesturechange\",\n      onGestureChange as EventListener,\n    );\n    document.removeEventListener(\"gestureend\", onGestureEnd as EventListener);\n  }, [onGestureChange]);\n\n  const onGestureStart = React.useCallback(\n    (event: GestureEvent) => {\n      event.preventDefault();\n      document.addEventListener(\n        \"gesturechange\",\n        onGestureChange as EventListener,\n      );\n      document.addEventListener(\"gestureend\", onGestureEnd as EventListener);\n      gestureZoomStartRef.current = zoom;\n      gestureRotationStartRef.current = rotation;\n    },\n    [zoom, rotation, onGestureChange, onGestureEnd],\n  );\n\n  const onSafariZoomPrevent = React.useCallback(\n    (event: Event) => event.preventDefault(),\n    [],\n  );\n\n  const onEventsCleanup = React.useCallback(() => {\n    document.removeEventListener(\"mousemove\", onMouseMove);\n    document.removeEventListener(\"touchmove\", onTouchMove);\n    document.removeEventListener(\n      \"gesturechange\",\n      onGestureChange as EventListener,\n    );\n    document.removeEventListener(\"gestureend\", onGestureEnd as EventListener);\n  }, [onMouseMove, onTouchMove, onGestureChange, onGestureEnd]);\n\n  const onDragStopped = React.useCallback(() => {\n    isTouchingRef.current = false;\n    store.setState(\"isDragging\", false);\n    onRefsCleanup();\n    document.removeEventListener(\"mouseup\", onDragStopped);\n    document.removeEventListener(\"touchend\", onDragStopped);\n    onEventsCleanup();\n  }, [store, onEventsCleanup, onRefsCleanup]);\n\n  const getWheelDelta = React.useCallback((event: WheelEvent) => {\n    let deltaX = event.deltaX;\n    let deltaY = event.deltaY;\n    let deltaZ = event.deltaZ;\n\n    if (event.deltaMode === 1) {\n      deltaX *= 16;\n      deltaY *= 16;\n      deltaZ *= 16;\n    } else if (event.deltaMode === 2) {\n      deltaX *= 400;\n      deltaY *= 400;\n      deltaZ *= 400;\n    }\n\n    return { deltaX, deltaY, deltaZ };\n  }, []);\n\n  const onWheelZoom = React.useCallback(\n    (event: WheelEvent) => {\n      propsRef.current.onWheelZoom?.(event);\n      if (event.defaultPrevented) return;\n\n      event.preventDefault();\n      const point = getMousePoint(event);\n      const { deltaY } = getWheelDelta(event);\n      const newZoom = zoom - (deltaY * context.zoomSpeed) / 200;\n      onZoomChange(newZoom, point, true);\n\n      store.batch(() => {\n        const currentState = store.getState();\n        if (!currentState.isWheelZooming) {\n          store.setState(\"isWheelZooming\", true);\n        }\n        if (!currentState.isDragging) {\n          store.setState(\"isDragging\", true);\n        }\n      });\n\n      if (wheelTimerRef.current) {\n        clearTimeout(wheelTimerRef.current);\n      }\n      wheelTimerRef.current = window.setTimeout(() => {\n        store.batch(() => {\n          store.setState(\"isWheelZooming\", false);\n          store.setState(\"isDragging\", false);\n        });\n      }, 250);\n    },\n    [\n      propsRef,\n      getMousePoint,\n      zoom,\n      context.zoomSpeed,\n      onZoomChange,\n      getWheelDelta,\n      store,\n    ],\n  );\n\n  const onKeyUp = React.useCallback(\n    (event: React.KeyboardEvent<RootElement>) => {\n      propsRef.current.onKeyUp?.(event);\n      if (event.defaultPrevented) return;\n\n      const arrowKeys = new Set([\n        \"ArrowUp\",\n        \"ArrowDown\",\n        \"ArrowLeft\",\n        \"ArrowRight\",\n      ]);\n\n      if (arrowKeys.has(event.key)) {\n        event.preventDefault();\n        store.setState(\"isDragging\", false);\n      }\n    },\n    [propsRef, store],\n  );\n\n  const onKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<RootElement>) => {\n      propsRef.current.onKeyDown?.(event);\n      if (event.defaultPrevented || !cropSize || !mediaSize) return;\n\n      let step = context.keyboardStep;\n      if (event.shiftKey) {\n        step *= 0.2;\n      }\n\n      const keyCallbacks: Record<string, () => Point> = {\n        ArrowUp: () => ({ ...crop, y: crop.y - step }),\n        ArrowDown: () => ({ ...crop, y: crop.y + step }),\n        ArrowLeft: () => ({ ...crop, x: crop.x - step }),\n        ArrowRight: () => ({ ...crop, x: crop.x + step }),\n      } as const;\n\n      const callback = keyCallbacks[event.key];\n      if (!callback) return;\n\n      event.preventDefault();\n\n      let newCrop = callback();\n\n      if (!context.allowOverflow) {\n        newCrop = onPositionClamp(newCrop, mediaSize, cropSize, zoom, rotation);\n      }\n\n      if (!event.repeat) {\n        store.setState(\"isDragging\", true);\n      }\n\n      store.setState(\"crop\", newCrop);\n    },\n    [\n      propsRef,\n      cropSize,\n      mediaSize,\n      context.keyboardStep,\n      context.allowOverflow,\n      crop,\n      zoom,\n      rotation,\n      store,\n    ],\n  );\n\n  const onMouseDown = React.useCallback(\n    (event: React.MouseEvent<RootElement>) => {\n      propsRef.current.onMouseDown?.(event);\n      if (event.defaultPrevented) return;\n\n      event.preventDefault();\n      document.addEventListener(\"mousemove\", onMouseMove);\n      document.addEventListener(\"mouseup\", onDragStopped);\n      onContentPositionChange();\n      onDragStart(getMousePoint(event));\n    },\n    [\n      propsRef,\n      getMousePoint,\n      onDragStart,\n      onDragStopped,\n      onMouseMove,\n      onContentPositionChange,\n    ],\n  );\n\n  const onTouchStart = React.useCallback(\n    (event: React.TouchEvent<RootElement>) => {\n      propsRef.current.onTouchStart?.(event);\n      if (event.defaultPrevented) return;\n\n      isTouchingRef.current = true;\n      document.addEventListener(\"touchmove\", onTouchMove, { passive: false });\n      document.addEventListener(\"touchend\", onDragStopped);\n      onContentPositionChange();\n\n      if (event.touches.length === 2) {\n        const [firstTouch, secondTouch] = event.touches\n          ? Array.from(event.touches)\n          : [];\n        if (firstTouch && secondTouch) {\n          const pointA = getTouchPoint(firstTouch);\n          const pointB = getTouchPoint(secondTouch);\n          lastPinchDistanceRef.current = getDistanceBetweenPoints(\n            pointA,\n            pointB,\n          );\n          lastPinchRotationRef.current = getRotationBetweenPoints(\n            pointA,\n            pointB,\n          );\n          onDragStart(getCenter(pointA, pointB));\n        }\n      } else if (event.touches.length === 1) {\n        const firstTouch = event.touches[0];\n        if (firstTouch) {\n          onDragStart(getTouchPoint(firstTouch));\n        }\n      }\n    },\n    [\n      propsRef,\n      onDragStopped,\n      onTouchMove,\n      onContentPositionChange,\n      getTouchPoint,\n      onDragStart,\n    ],\n  );\n\n  React.useEffect(() => {\n    const content = context.rootRef?.current;\n    if (!content) return;\n\n    if (!context.preventScrollZoom) {\n      content.addEventListener(\"wheel\", onWheelZoom, { passive: false });\n    }\n\n    content.addEventListener(\"gesturestart\", onSafariZoomPrevent);\n    content.addEventListener(\"gesturestart\", onGestureStart as EventListener);\n\n    return () => {\n      if (!context.preventScrollZoom) {\n        content.removeEventListener(\"wheel\", onWheelZoom);\n      }\n      content.removeEventListener(\"gesturestart\", onSafariZoomPrevent);\n      content.removeEventListener(\n        \"gesturestart\",\n        onGestureStart as EventListener,\n      );\n      onRefsCleanup();\n    };\n  }, [\n    context.rootRef,\n    context.preventScrollZoom,\n    onWheelZoom,\n    onRefsCleanup,\n    onSafariZoomPrevent,\n    onGestureStart,\n  ]);\n\n  React.useEffect(() => {\n    return () => {\n      onRefsCleanup();\n      onCacheCleanup();\n    };\n  }, [onRefsCleanup, onCacheCleanup]);\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <RootPrimitive\n      data-slot=\"cropper\"\n      tabIndex={0}\n      {...rootImplProps}\n      ref={composedRef}\n      className={cn(\n        \"absolute inset-0 flex cursor-move touch-none items-center justify-center overflow-hidden outline-none select-none\",\n        className,\n      )}\n      onKeyUp={onKeyUp}\n      onKeyDown={onKeyDown}\n      onMouseDown={onMouseDown}\n      onTouchStart={onTouchStart}\n    />\n  );\n}\n\nconst cropperMediaVariants = cva(\"will-change-transform\", {\n  variants: {\n    objectFit: {\n      contain: \"absolute inset-0 m-auto max-h-full max-w-full\",\n      cover: \"h-auto w-full\",\n      \"horizontal-cover\": \"h-auto w-full\",\n      \"vertical-cover\": \"h-full w-auto\",\n    },\n  },\n  defaultVariants: {\n    objectFit: \"contain\",\n  },\n});\n\ninterface UseMediaComputationProps<\n  T extends HTMLImageElement | HTMLVideoElement,\n> {\n  mediaRef: React.RefObject<T | null>;\n  context: CropperContextValue;\n  store: Store;\n  rotation: number;\n  getNaturalDimensions: (media: T) => Size;\n}\n\nfunction useMediaComputation<T extends HTMLImageElement | HTMLVideoElement>({\n  mediaRef,\n  context,\n  store,\n  rotation,\n  getNaturalDimensions,\n}: UseMediaComputationProps<T>) {\n  const computeSizes = React.useCallback(() => {\n    const media = mediaRef.current;\n    const content = context.rootRef?.current;\n    if (!media || !content) return;\n\n    const contentRect = content.getBoundingClientRect();\n    const containerAspect = contentRect.width / contentRect.height;\n    const { width: naturalWidth, height: naturalHeight } =\n      getNaturalDimensions(media);\n    const isScaledDown =\n      media.offsetWidth < naturalWidth || media.offsetHeight < naturalHeight;\n    const mediaAspect = naturalWidth / naturalHeight;\n\n    let renderedMediaSize: Size;\n\n    if (isScaledDown) {\n      const objectFitCallbacks = {\n        contain: () =>\n          containerAspect > mediaAspect\n            ? {\n                width: contentRect.height * mediaAspect,\n                height: contentRect.height,\n              }\n            : {\n                width: contentRect.width,\n                height: contentRect.width / mediaAspect,\n              },\n        \"horizontal-cover\": () => ({\n          width: contentRect.width,\n          height: contentRect.width / mediaAspect,\n        }),\n        \"vertical-cover\": () => ({\n          width: contentRect.height * mediaAspect,\n          height: contentRect.height,\n        }),\n        cover: () =>\n          containerAspect < mediaAspect\n            ? {\n                width: contentRect.width,\n                height: contentRect.width / mediaAspect,\n              }\n            : {\n                width: contentRect.height * mediaAspect,\n                height: contentRect.height,\n              },\n      } as const;\n\n      const callback = objectFitCallbacks[context.objectFit];\n      renderedMediaSize = callback\n        ? callback()\n        : containerAspect > mediaAspect\n          ? {\n              width: contentRect.height * mediaAspect,\n              height: contentRect.height,\n            }\n          : {\n              width: contentRect.width,\n              height: contentRect.width / mediaAspect,\n            };\n    } else {\n      renderedMediaSize = {\n        width: media.offsetWidth,\n        height: media.offsetHeight,\n      };\n    }\n\n    const mediaSize: MediaSize = {\n      ...renderedMediaSize,\n      naturalWidth,\n      naturalHeight,\n    };\n\n    store.setState(\"mediaSize\", mediaSize);\n\n    const cropSize = getCropSize(\n      mediaSize.width,\n      mediaSize.height,\n      contentRect.width,\n      contentRect.height,\n      context.aspectRatio,\n      rotation,\n    );\n\n    store.setState(\"cropSize\", cropSize);\n\n    requestAnimationFrame(() => {\n      const currentState = store.getState();\n      if (currentState.cropSize && currentState.mediaSize) {\n        const newPosition = onPositionClamp(\n          currentState.crop,\n          currentState.mediaSize,\n          currentState.cropSize,\n          currentState.zoom,\n          currentState.rotation,\n        );\n\n        if (\n          Math.abs(newPosition.x - currentState.crop.x) > 0.001 ||\n          Math.abs(newPosition.y - currentState.crop.y) > 0.001\n        ) {\n          store.setState(\"crop\", newPosition);\n        }\n      }\n    });\n\n    return { mediaSize, cropSize };\n  }, [\n    mediaRef,\n    context.aspectRatio,\n    context.rootRef,\n    context.objectFit,\n    store,\n    rotation,\n    getNaturalDimensions,\n  ]);\n\n  return { computeSizes };\n}\n\ninterface CropperImageProps\n  extends\n    React.ComponentProps<\"img\">,\n    VariantProps<typeof cropperMediaVariants> {\n  asChild?: boolean;\n  snapPixels?: boolean;\n}\n\nfunction CropperImage(props: CropperImageProps) {\n  const {\n    className,\n    style,\n    asChild,\n    ref,\n    onLoad,\n    objectFit,\n    snapPixels = false,\n    ...imageProps\n  } = props;\n\n  const context = useCropperContext(IMAGE_NAME);\n  const store = useStoreContext(IMAGE_NAME);\n  const crop = useStore((state) => state.crop);\n  const zoom = useStore((state) => state.zoom);\n  const rotation = useStore((state) => state.rotation);\n\n  const imageRef = React.useRef<HTMLImageElement>(null);\n  const composedRef = useComposedRefs(ref, imageRef);\n\n  const getNaturalDimensions = React.useCallback(\n    (image: HTMLImageElement) => ({\n      width: image.naturalWidth,\n      height: image.naturalHeight,\n    }),\n    [],\n  );\n\n  const { computeSizes } = useMediaComputation({\n    mediaRef: imageRef,\n    context,\n    store,\n    rotation,\n    getNaturalDimensions,\n  });\n\n  const onMediaLoad = React.useCallback(() => {\n    const image = imageRef.current;\n    if (!image) return;\n\n    computeSizes();\n\n    onLoad?.(\n      new Event(\"load\") as unknown as React.SyntheticEvent<HTMLImageElement>,\n    );\n  }, [computeSizes, onLoad]);\n\n  React.useEffect(() => {\n    const image = imageRef.current;\n    if (image?.complete && image.naturalWidth > 0) {\n      onMediaLoad();\n    }\n  }, [onMediaLoad]);\n\n  React.useEffect(() => {\n    const content = context.rootRef?.current;\n    if (!content) return;\n\n    if (typeof ResizeObserver !== \"undefined\") {\n      let isFirstResize = true;\n      const resizeObserver = new ResizeObserver(() => {\n        if (isFirstResize) {\n          isFirstResize = false;\n          return;\n        }\n\n        const callback = () => {\n          const image = imageRef.current;\n          if (image?.complete && image.naturalWidth > 0) {\n            computeSizes();\n          }\n        };\n\n        if (\"requestIdleCallback\" in window) {\n          requestIdleCallback(callback);\n        } else {\n          setTimeout(callback, 16);\n        }\n      });\n\n      resizeObserver.observe(content);\n\n      return () => {\n        resizeObserver.disconnect();\n      };\n    } else {\n      const onWindowResize = () => {\n        const image = imageRef.current;\n        if (image?.complete && image.naturalWidth > 0) {\n          computeSizes();\n        }\n      };\n\n      window.addEventListener(\"resize\", onWindowResize);\n      return () => {\n        window.removeEventListener(\"resize\", onWindowResize);\n      };\n    }\n  }, [context.rootRef, computeSizes]);\n\n  const ImagePrimitive = asChild ? SlotPrimitive.Slot : \"img\";\n\n  return (\n    <ImagePrimitive\n      data-slot=\"cropper-image\"\n      {...imageProps}\n      ref={composedRef}\n      className={cn(\n        cropperMediaVariants({\n          objectFit: objectFit ?? context.objectFit,\n          className,\n        }),\n      )}\n      style={{\n        transform: snapPixels\n          ? `translate(${snapToDevicePixel(crop.x)}px, ${snapToDevicePixel(crop.y)}px) rotate(${rotation}deg) scale(${zoom})`\n          : `translate(${crop.x}px, ${crop.y}px) rotate(${rotation}deg) scale(${zoom})`,\n        ...style,\n      }}\n      onLoad={onMediaLoad}\n    />\n  );\n}\n\ninterface CropperVideoProps\n  extends\n    React.ComponentProps<\"video\">,\n    VariantProps<typeof cropperMediaVariants> {\n  asChild?: boolean;\n  snapPixels?: boolean;\n}\n\nfunction CropperVideo(props: CropperVideoProps) {\n  const {\n    className,\n    style,\n    asChild,\n    ref,\n    onLoadedMetadata,\n    objectFit,\n    snapPixels = false,\n    ...videoProps\n  } = props;\n\n  const context = useCropperContext(VIDEO_NAME);\n  const store = useStoreContext(VIDEO_NAME);\n  const crop = useStore((state) => state.crop);\n  const zoom = useStore((state) => state.zoom);\n  const rotation = useStore((state) => state.rotation);\n\n  const videoRef = React.useRef<HTMLVideoElement>(null);\n  const composedRef = useComposedRefs(ref, videoRef);\n\n  const getNaturalDimensions = React.useCallback(\n    (video: HTMLVideoElement) => ({\n      width: video.videoWidth,\n      height: video.videoHeight,\n    }),\n    [],\n  );\n\n  const { computeSizes } = useMediaComputation({\n    mediaRef: videoRef,\n    context,\n    store,\n    rotation,\n    getNaturalDimensions,\n  });\n\n  const onMediaLoad = React.useCallback(() => {\n    const video = videoRef.current;\n    if (!video) return;\n\n    computeSizes();\n\n    onLoadedMetadata?.(\n      new Event(\n        \"loadedmetadata\",\n      ) as unknown as React.SyntheticEvent<HTMLVideoElement>,\n    );\n  }, [computeSizes, onLoadedMetadata]);\n\n  React.useEffect(() => {\n    const content = context.rootRef?.current;\n    if (!content) return;\n\n    if (typeof ResizeObserver !== \"undefined\") {\n      let isFirstResize = true;\n      const resizeObserver = new ResizeObserver(() => {\n        if (isFirstResize) {\n          isFirstResize = false;\n          return;\n        }\n\n        const callback = () => {\n          const video = videoRef.current;\n          if (video && video.videoWidth > 0 && video.videoHeight > 0) {\n            computeSizes();\n          }\n        };\n\n        if (\"requestIdleCallback\" in window) {\n          requestIdleCallback(callback);\n        } else {\n          setTimeout(callback, 16);\n        }\n      });\n\n      resizeObserver.observe(content);\n\n      return () => {\n        resizeObserver.disconnect();\n      };\n    } else {\n      const onWindowResize = () => {\n        const video = videoRef.current;\n        if (video && video.videoWidth > 0 && video.videoHeight > 0) {\n          computeSizes();\n        }\n      };\n\n      window.addEventListener(\"resize\", onWindowResize);\n      return () => {\n        window.removeEventListener(\"resize\", onWindowResize);\n      };\n    }\n  }, [context.rootRef, computeSizes]);\n\n  const VideoPrimitive = asChild ? SlotPrimitive.Slot : \"video\";\n\n  return (\n    <VideoPrimitive\n      data-slot=\"cropper-video\"\n      autoPlay\n      playsInline\n      loop\n      muted\n      controls={false}\n      {...videoProps}\n      ref={composedRef}\n      className={cn(\n        cropperMediaVariants({\n          objectFit: objectFit ?? context.objectFit,\n          className,\n        }),\n      )}\n      style={{\n        transform: snapPixels\n          ? `translate(${snapToDevicePixel(crop.x)}px, ${snapToDevicePixel(crop.y)}px) rotate(${rotation}deg) scale(${zoom})`\n          : `translate(${crop.x}px, ${crop.y}px) rotate(${rotation}deg) scale(${zoom})`,\n        ...style,\n      }}\n      onLoadedMetadata={onMediaLoad}\n    />\n  );\n}\n\nconst cropperAreaVariants = cva(\n  \"absolute top-1/2 left-1/2 box-border -translate-x-1/2 -translate-y-1/2 overflow-hidden border border-[2.5px] border-white/90 shadow-[0_0_0_9999em_rgba(0,0,0,0.5)]\",\n  {\n    variants: {\n      shape: {\n        rectangle: \"\",\n        circle: \"rounded-full\",\n      },\n      withGrid: {\n        true: \"before:absolute before:top-0 before:right-1/3 before:bottom-0 before:left-1/3 before:box-border before:border before:border-t-0 before:border-b-0 before:border-white/50 before:content-[''] after:absolute after:top-1/3 after:right-0 after:bottom-1/3 after:left-0 after:box-border after:border after:border-r-0 after:border-l-0 after:border-white/50 after:content-['']\",\n        false: \"\",\n      },\n    },\n    defaultVariants: {\n      shape: \"rectangle\",\n      withGrid: false,\n    },\n  },\n);\n\ninterface CropperAreaProps\n  extends DivProps, VariantProps<typeof cropperAreaVariants> {\n  snapPixels?: boolean;\n}\n\nfunction CropperArea(props: CropperAreaProps) {\n  const {\n    className,\n    style,\n    asChild,\n    ref,\n    snapPixels = false,\n    shape,\n    withGrid,\n    ...areaProps\n  } = props;\n\n  const context = useCropperContext(AREA_NAME);\n  const cropSize = useStore((state) => state.cropSize);\n\n  if (!cropSize) return null;\n\n  const AreaPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <AreaPrimitive\n      data-slot=\"cropper-area\"\n      {...areaProps}\n      ref={ref}\n      className={cn(\n        cropperAreaVariants({\n          shape: shape ?? context.shape,\n          withGrid: withGrid ?? context.withGrid,\n          className,\n        }),\n      )}\n      style={{\n        width: snapPixels ? Math.round(cropSize.width) : cropSize.width,\n        height: snapPixels ? Math.round(cropSize.height) : cropSize.height,\n        ...style,\n      }}\n    />\n  );\n}\n\nexport {\n  type Area as CropperAreaData,\n  Cropper,\n  CropperArea,\n  CropperImage,\n  type CropperProps,\n  CropperVideo,\n  type ObjectFit as CropperObjectFit,\n  type Point as CropperPoint,\n  type Shape as CropperShape,\n  type Size as CropperSize,\n  useStore as useCropper,\n};\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}