{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-crop",
  "title": "Image Crop",
  "description": "A responsive, pointer- and keyboard-driven image cropping tool. Draw, drag, and 8-way resize a crop over any image or video, with fixed aspect ratios, circular crops, min/max bounds, rule-of-thirds guides, and percent/pixel crop callbacks. Self-contained Tailwind styling, zero dependencies.",
  "dependencies": [
    "cn"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/ikui/image-crop.tsx",
      "content": "'use client'\n\nimport { cn } from 'cn'\nimport type {\n  CSSProperties,\n  HTMLAttributes,\n  KeyboardEvent,\n  PointerEvent,\n  ReactNode,\n} from 'react'\nimport { useEffect, useId, useRef, useState } from 'react'\n\nexport type Ords = 'n' | 's' | 'e' | 'w' | 'nw' | 'ne' | 'se' | 'sw'\n\nexport interface Crop {\n  x: number\n  y: number\n  width: number\n  height: number\n  unit: 'px' | '%'\n}\n\nexport interface PixelCrop extends Crop {\n  unit: 'px'\n}\n\nexport interface PercentCrop extends Crop {\n  unit: '%'\n}\n\nconst defaultCrop: PixelCrop = { x: 0, y: 0, width: 0, height: 0, unit: 'px' }\n\nconst NUDGE_STEP = 1\nconst NUDGE_STEP_MEDIUM = 10\nconst NUDGE_STEP_LARGE = 100\n\n// ---------------------------------------------------------------------------\n// Geometry helpers (pure). Ported from react-image-crop — battle-tested math.\n// ---------------------------------------------------------------------------\n\nconst clamp = (num: number, min: number, max: number) =>\n  Math.min(Math.max(num, min), max)\n\nconst areCropsEqual = (a: Partial<Crop>, b: Partial<Crop>) =>\n  a === b ||\n  (a.width === b.width &&\n    a.height === b.height &&\n    a.x === b.x &&\n    a.y === b.y &&\n    a.unit === b.unit)\n\nexport function convertToPercentCrop(\n  crop: Partial<Crop>,\n  containerWidth: number,\n  containerHeight: number,\n): PercentCrop {\n  if (crop.unit === '%') {\n    return { ...defaultCrop, ...crop, unit: '%' }\n  }\n\n  return {\n    unit: '%',\n    x: crop.x ? (crop.x / containerWidth) * 100 : 0,\n    y: crop.y ? (crop.y / containerHeight) * 100 : 0,\n    width: crop.width ? (crop.width / containerWidth) * 100 : 0,\n    height: crop.height ? (crop.height / containerHeight) * 100 : 0,\n  }\n}\n\nexport function convertToPixelCrop(\n  crop: Partial<Crop>,\n  containerWidth: number,\n  containerHeight: number,\n): PixelCrop {\n  if (!crop.unit || crop.unit === 'px') {\n    return { ...defaultCrop, ...crop, unit: 'px' }\n  }\n\n  return {\n    unit: 'px',\n    x: crop.x ? (crop.x * containerWidth) / 100 : 0,\n    y: crop.y ? (crop.y * containerHeight) / 100 : 0,\n    width: crop.width ? (crop.width * containerWidth) / 100 : 0,\n    height: crop.height ? (crop.height * containerHeight) / 100 : 0,\n  }\n}\n\nexport function makeAspectCrop(\n  crop: Pick<PercentCrop, 'unit'> & Partial<Omit<PercentCrop, 'unit'>>,\n  aspect: number,\n  containerWidth: number,\n  containerHeight: number,\n): PercentCrop\nexport function makeAspectCrop(\n  crop: Pick<PixelCrop, 'unit'> & Partial<Omit<PixelCrop, 'unit'>>,\n  aspect: number,\n  containerWidth: number,\n  containerHeight: number,\n): PixelCrop\nexport function makeAspectCrop(\n  crop: Partial<Crop>,\n  aspect: number,\n  containerWidth: number,\n  containerHeight: number,\n) {\n  const pixelCrop = convertToPixelCrop(crop, containerWidth, containerHeight)\n\n  if (crop.width) {\n    pixelCrop.height = pixelCrop.width / aspect\n  }\n\n  if (crop.height) {\n    pixelCrop.width = pixelCrop.height * aspect\n  }\n\n  if (pixelCrop.y + pixelCrop.height > containerHeight) {\n    pixelCrop.height = containerHeight - pixelCrop.y\n    pixelCrop.width = pixelCrop.height * aspect\n  }\n\n  if (pixelCrop.x + pixelCrop.width > containerWidth) {\n    pixelCrop.width = containerWidth - pixelCrop.x\n    pixelCrop.height = pixelCrop.width / aspect\n  }\n\n  return crop.unit === '%'\n    ? convertToPercentCrop(pixelCrop, containerWidth, containerHeight)\n    : pixelCrop\n}\n\nexport function centerCrop(\n  crop: Pick<PercentCrop, 'unit'> & Partial<Omit<PercentCrop, 'unit'>>,\n  containerWidth: number,\n  containerHeight: number,\n): PercentCrop\nexport function centerCrop(\n  crop: Pick<PixelCrop, 'unit'> & Partial<Omit<PixelCrop, 'unit'>>,\n  containerWidth: number,\n  containerHeight: number,\n): PixelCrop\nexport function centerCrop(\n  crop: Partial<Crop>,\n  containerWidth: number,\n  containerHeight: number,\n) {\n  const pixelCrop = convertToPixelCrop(crop, containerWidth, containerHeight)\n\n  pixelCrop.x = (containerWidth - pixelCrop.width) / 2\n  pixelCrop.y = (containerHeight - pixelCrop.height) / 2\n\n  return crop.unit === '%'\n    ? convertToPercentCrop(pixelCrop, containerWidth, containerHeight)\n    : pixelCrop\n}\n\nfunction containCrop(\n  pixelCrop: PixelCrop,\n  aspect: number,\n  ord: Ords,\n  containerWidth: number,\n  containerHeight: number,\n  minWidth = 0,\n  minHeight = 0,\n  maxWidth = containerWidth,\n  maxHeight = containerHeight,\n): PixelCrop {\n  const next = { ...pixelCrop }\n  let minW = Math.min(minWidth, containerWidth)\n  let minH = Math.min(minHeight, containerHeight)\n  let maxW = Math.min(maxWidth, containerWidth)\n  let maxH = Math.min(maxHeight, containerHeight)\n\n  if (aspect) {\n    if (aspect > 1) {\n      // Landscape — drive off width.\n      minW = minHeight ? minHeight * aspect : minW\n      minH = minW / aspect\n      maxW = maxWidth * aspect\n    } else {\n      // Portrait — drive off height.\n      minH = minWidth ? minWidth / aspect : minH\n      minW = minH * aspect\n      maxH = maxHeight / aspect\n    }\n  }\n\n  // Stop underflow on top / left.\n  if (next.y < 0) {\n    next.height = Math.max(next.height + next.y, minH)\n    next.y = 0\n  }\n  if (next.x < 0) {\n    next.width = Math.max(next.width + next.x, minW)\n    next.x = 0\n  }\n\n  // Stop overflow on right / bottom.\n  const xOverflow = containerWidth - (next.x + next.width)\n  if (xOverflow < 0) {\n    next.x = Math.min(next.x, containerWidth - minW)\n    next.width += xOverflow\n  }\n  const yOverflow = containerHeight - (next.y + next.height)\n  if (yOverflow < 0) {\n    next.y = Math.min(next.y, containerHeight - minH)\n    next.height += yOverflow\n  }\n\n  // Respect min / max, anchoring the opposite edge when the dragged edge hits a limit.\n  if (next.width < minW) {\n    if (ord === 'sw' || ord === 'nw') next.x -= minW - next.width\n    next.width = minW\n  }\n  if (next.height < minH) {\n    if (ord === 'nw' || ord === 'ne') next.y -= minH - next.height\n    next.height = minH\n  }\n  if (next.width > maxW) {\n    if (ord === 'sw' || ord === 'nw') next.x -= maxW - next.width\n    next.width = maxW\n  }\n  if (next.height > maxH) {\n    if (ord === 'nw' || ord === 'ne') next.y -= maxH - next.height\n    next.height = maxH\n  }\n\n  // Re-apply aspect after the size fixes above.\n  if (aspect) {\n    const currAspect = next.width / next.height\n    if (currAspect < aspect) {\n      const newHeight = Math.max(next.width / aspect, minH)\n      if (ord === 'nw' || ord === 'ne') next.y -= newHeight - next.height\n      next.height = newHeight\n    } else if (currAspect > aspect) {\n      const newWidth = Math.max(next.height * aspect, minW)\n      if (ord === 'sw' || ord === 'nw') next.x -= newWidth - next.width\n      next.width = newWidth\n    }\n  }\n\n  return next\n}\n\nfunction nudgeCrop(\n  pixelCrop: PixelCrop,\n  key: string,\n  offset: number,\n  ord: Ords,\n): PixelCrop {\n  const next = { ...pixelCrop }\n\n  if (key === 'ArrowLeft') {\n    if (ord === 'nw') {\n      next.x -= offset\n      next.y -= offset\n      next.width += offset\n      next.height += offset\n    } else if (ord === 'w') {\n      next.x -= offset\n      next.width += offset\n    } else if (ord === 'sw') {\n      next.x -= offset\n      next.width += offset\n      next.height += offset\n    } else if (ord === 'ne') {\n      next.y += offset\n      next.width -= offset\n      next.height -= offset\n    } else if (ord === 'e') {\n      next.width -= offset\n    } else if (ord === 'se') {\n      next.width -= offset\n      next.height -= offset\n    }\n  } else if (key === 'ArrowRight') {\n    if (ord === 'nw') {\n      next.x += offset\n      next.y += offset\n      next.width -= offset\n      next.height -= offset\n    } else if (ord === 'w') {\n      next.x += offset\n      next.width -= offset\n    } else if (ord === 'sw') {\n      next.x += offset\n      next.width -= offset\n      next.height -= offset\n    } else if (ord === 'ne') {\n      next.y -= offset\n      next.width += offset\n      next.height += offset\n    } else if (ord === 'e') {\n      next.width += offset\n    } else if (ord === 'se') {\n      next.width += offset\n      next.height += offset\n    }\n  } else if (key === 'ArrowUp') {\n    if (ord === 'nw') {\n      next.x -= offset\n      next.y -= offset\n      next.width += offset\n      next.height += offset\n    } else if (ord === 'n') {\n      next.y -= offset\n      next.height += offset\n    } else if (ord === 'ne') {\n      next.y -= offset\n      next.width += offset\n      next.height += offset\n    } else if (ord === 'sw') {\n      next.x += offset\n      next.width -= offset\n      next.height -= offset\n    } else if (ord === 's') {\n      next.height -= offset\n    } else if (ord === 'se') {\n      next.width -= offset\n      next.height -= offset\n    }\n  } else if (key === 'ArrowDown') {\n    if (ord === 'nw') {\n      next.x += offset\n      next.y += offset\n      next.width -= offset\n      next.height -= offset\n    } else if (ord === 'n') {\n      next.y += offset\n      next.height -= offset\n    } else if (ord === 'ne') {\n      next.y += offset\n      next.width -= offset\n      next.height -= offset\n    } else if (ord === 'sw') {\n      next.x -= offset\n      next.width += offset\n      next.height += offset\n    } else if (ord === 's') {\n      next.height += offset\n    } else if (ord === 'se') {\n      next.width += offset\n      next.height += offset\n    }\n  }\n\n  return next\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\ninterface Rect {\n  /** Left edge in CLIENT (screen) pixels — where pointer events report. */\n  x: number\n  /** Top edge in CLIENT (screen) pixels. */\n  y: number\n  /** Width in LAYOUT pixels — the space crop values live in. */\n  width: number\n  /** Height in LAYOUT pixels. */\n  height: number\n  /** Client pixels per layout pixel, from any scaled ancestor. */\n  scaleX: number\n  scaleY: number\n}\n\n/**\n * A client coordinate expressed in the media's own layout pixels.\n *\n * Pointer events report SCREEN pixels, while crop values and the selection's\n * own `left`/`width` style are LAYOUT pixels. The two are the same number only\n * when no ancestor is scaled — and this component is used inside a canvas that\n * zooms the whole flow with one CSS transform, where mixing them shrinks the\n * crop by the zoom factor and drags it toward the origin.\n */\nfunction toLocalX(box: Rect, clientX: number): number {\n  return (clientX - box.x) / box.scaleX\n}\n\nfunction toLocalY(box: Rect, clientY: number): number {\n  return (clientY - box.y) / box.scaleY\n}\n\n/** A client-space distance in the media's own layout pixels. */\nfunction toLocalDistanceX(box: Rect, distance: number): number {\n  return distance / box.scaleX\n}\n\nfunction toLocalDistanceY(box: Rect, distance: number): number {\n  return distance / box.scaleY\n}\n\ninterface DragData {\n  startClientX: number\n  startClientY: number\n  startCropX: number\n  startCropY: number\n  clientX: number\n  clientY: number\n  isResize: boolean\n  ord?: Ords\n}\n\nexport interface ImageCropState {\n  cropIsActive: boolean\n  newCropIsBeingDrawn: boolean\n}\n\nexport interface ImageCropProps\n  extends Omit<\n    HTMLAttributes<HTMLDivElement>,\n    | 'onChange'\n    | 'onDragStart'\n    | 'onDragEnd'\n    | 'tabIndex'\n    | 'onPointerMove'\n    | 'onPointerUp'\n    | 'onPointerCancel'\n  > {\n  /** The element to crop, e.g. an `<img>` or `<video>`. */\n  children?: ReactNode\n  /** The current crop. Omit entirely for no crop. This is a controlled value:\n   *  set it from `onChange` and pass it back in. */\n  crop?: Crop\n  /** The aspect ratio of the crop, e.g. `1` (square) or `16 / 9` (landscape). */\n  aspect?: number\n  /** Render the crop area as a circle (warps to an oval if `aspect` is not 1). */\n  circularCrop?: boolean\n  /** Disable all interaction. */\n  disabled?: boolean\n  /** Prevent creating or resizing a crop, but still allow dragging it around. */\n  locked?: boolean\n  /** Prevent the crop from being cleared when clicking outside it. */\n  keepSelection?: boolean\n  /** Minimum crop width, in pixels. */\n  minWidth?: number\n  /** Minimum crop height, in pixels. */\n  minHeight?: number\n  /** Maximum crop width, in pixels. */\n  maxWidth?: number\n  /** Maximum crop height, in pixels. */\n  maxHeight?: number\n  /** Show rule-of-thirds guide lines inside the crop. */\n  ruleOfThirds?: boolean\n  /** Fired on every crop change. Set the crop to state and pass it back. */\n  onChange: (crop: PixelCrop, percentCrop: PercentCrop) => void\n  /** Fired after a resize, drag, or keyboard nudge completes. */\n  onComplete?: (crop: PixelCrop, percentCrop: PercentCrop) => void\n  /** Fired when the user starts dragging or resizing. */\n  onDragStart?: (e: PointerEvent<HTMLDivElement>) => void\n  /** Fired when the user releases after dragging or resizing. */\n  onDragEnd?: (e: PointerEvent<HTMLDivElement>) => void\n  /** Render a custom element inside the crop selection. */\n  renderSelectionAddon?: (state: ImageCropState) => ReactNode\n  className?: string\n  style?: CSSProperties\n  /** Classes merged onto the crop selection box — override the border, etc. */\n  selectionClassName?: string\n  /** Classes merged onto each resize handle — override the size, color, etc. */\n  handleClassName?: string\n}\n\nconst HANDLE_CLASSES: Record<Ords, string> = {\n  n: 'left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 cursor-n-resize',\n  e: 'right-0 top-1/2 translate-x-1/2 -translate-y-1/2 cursor-e-resize',\n  s: 'bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 cursor-s-resize',\n  w: 'left-0 top-1/2 -translate-x-1/2 -translate-y-1/2 cursor-w-resize',\n  nw: 'left-0 top-0 -translate-x-1/2 -translate-y-1/2 cursor-nw-resize',\n  ne: 'right-0 top-0 translate-x-1/2 -translate-y-1/2 cursor-ne-resize',\n  se: 'bottom-0 right-0 translate-x-1/2 translate-y-1/2 cursor-se-resize',\n  sw: 'bottom-0 left-0 -translate-x-1/2 translate-y-1/2 cursor-sw-resize',\n}\n\nconst BAR_CLASSES: Record<'n' | 'e' | 's' | 'w', string> = {\n  n: 'left-0 top-0 h-1.5 w-full -translate-y-1/2 cursor-n-resize',\n  e: 'right-0 top-0 h-full w-1.5 translate-x-1/2 cursor-e-resize',\n  s: 'bottom-0 left-0 h-1.5 w-full translate-y-1/2 cursor-s-resize',\n  w: 'left-0 top-0 h-full w-1.5 -translate-x-1/2 cursor-w-resize',\n}\n\nconst ORD_LABEL: Record<Ords, string> = {\n  n: 'north',\n  e: 'east',\n  s: 'south',\n  w: 'west',\n  nw: 'north-west',\n  ne: 'north-east',\n  se: 'south-east',\n  sw: 'south-west',\n}\n\nconst CORNER_ORDS = ['nw', 'ne', 'se', 'sw'] as const\nconst EDGE_ORDS = ['n', 'e', 's', 'w'] as const\nconst xOrds: Ords[] = ['e', 'w']\nconst yOrds: Ords[] = ['n', 's']\nconst xyOrds: Ords[] = ['nw', 'ne', 'se', 'sw']\n\nexport function ImageCrop(props: ImageCropProps) {\n  const {\n    children,\n    crop,\n    aspect = 0,\n    circularCrop,\n    disabled,\n    locked,\n    keepSelection,\n    minWidth = 0,\n    minHeight = 0,\n    maxWidth,\n    maxHeight,\n    ruleOfThirds,\n    onChange,\n    onComplete,\n    onDragStart,\n    onDragEnd,\n    renderSelectionAddon,\n    className,\n    style,\n    selectionClassName,\n    handleClassName,\n    ...rest\n  } = props\n\n  const componentRef = useRef<HTMLDivElement>(null)\n  const mediaRef = useRef<HTMLDivElement>(null)\n  const selectionRef = useRef<HTMLDivElement>(null)\n  const maskId = useId()\n\n  const dragData = useRef<DragData>({\n    startClientX: 0,\n    startClientY: 0,\n    startCropX: 0,\n    startCropY: 0,\n    clientX: 0,\n    clientY: 0,\n    isResize: true,\n  })\n  const mouseDownOnCrop = useRef(false)\n  const dragStarted = useRef(false)\n  const pointerId = useRef<number | null>(null)\n\n  const [cropIsActive, setCropIsActive] = useState(false)\n  const [newCropIsBeingDrawn, setNewCropIsBeingDrawn] = useState(false)\n\n  // Fire onComplete once when a crop is set programmatically (no crop -> crop),\n  // so a preview can render without any user interaction.\n  const prevCrop = useRef(crop)\n  // biome-ignore lint/correctness/useExhaustiveDependencies: fire onComplete only on the no-crop -> crop transition\n  useEffect(() => {\n    if (onComplete && !prevCrop.current && crop) {\n      const box = getBox()\n      if (box.width && box.height) {\n        onComplete(\n          convertToPixelCrop(crop, box.width, box.height),\n          convertToPercentCrop(crop, box.width, box.height),\n        )\n      }\n    }\n    prevCrop.current = crop\n  }, [crop])\n\n  function getBox(): Rect {\n    const el = mediaRef.current\n    if (!el) {\n      return { x: 0, y: 0, width: 0, height: 0, scaleX: 1, scaleY: 1 }\n    }\n    const rect = el.getBoundingClientRect()\n    // `offsetWidth` is the layout size, the client rect is the painted one;\n    // their ratio IS the accumulated scale of every transformed ancestor.\n    const scaleX = el.offsetWidth > 0 ? rect.width / el.offsetWidth : 1\n    const scaleY = el.offsetHeight > 0 ? rect.height / el.offsetHeight : 1\n    return {\n      x: rect.x,\n      y: rect.y,\n      width: el.offsetWidth,\n      height: el.offsetHeight,\n      scaleX,\n      scaleY,\n    }\n  }\n\n  function makePixelCrop(box: Rect) {\n    return convertToPixelCrop(\n      { ...defaultCrop, ...(crop ?? {}) },\n      box.width,\n      box.height,\n    )\n  }\n\n  function capture(e: PointerEvent<HTMLDivElement>) {\n    componentRef.current?.setPointerCapture(e.pointerId)\n    pointerId.current = e.pointerId\n  }\n\n  function release() {\n    const id = pointerId.current\n    if (id !== null && componentRef.current?.hasPointerCapture(id)) {\n      componentRef.current.releasePointerCapture(id)\n    }\n    pointerId.current = null\n  }\n\n  function onCropPointerDown(e: PointerEvent<HTMLDivElement>) {\n    if (disabled || !crop) {\n      return\n    }\n    const box = getBox()\n    const pixelCrop = convertToPixelCrop(crop, box.width, box.height)\n\n    if (e.cancelable) e.preventDefault()\n    capture(e)\n    selectionRef.current?.focus({ preventScroll: true })\n\n    const ord = (e.target as HTMLElement).dataset.ord as Ords | undefined\n    const isResize = Boolean(ord)\n    let startClientX = e.clientX\n    let startClientY = e.clientY\n    let startCropX = pixelCrop.x\n    let startCropY = pixelCrop.y\n\n    // Anchor the drag to the opposite corner of the one being resized.\n    if (ord) {\n      const relativeX = toLocalX(box, e.clientX)\n      const relativeY = toLocalY(box, e.clientY)\n      let fromCornerX = 0\n      let fromCornerY = 0\n\n      if (ord === 'ne' || ord === 'e') {\n        fromCornerX = relativeX - (pixelCrop.x + pixelCrop.width)\n        fromCornerY = relativeY - pixelCrop.y\n        startCropX = pixelCrop.x\n        startCropY = pixelCrop.y + pixelCrop.height\n      } else if (ord === 'se' || ord === 's') {\n        fromCornerX = relativeX - (pixelCrop.x + pixelCrop.width)\n        fromCornerY = relativeY - (pixelCrop.y + pixelCrop.height)\n        startCropX = pixelCrop.x\n        startCropY = pixelCrop.y\n      } else if (ord === 'sw' || ord === 'w') {\n        fromCornerX = relativeX - pixelCrop.x\n        fromCornerY = relativeY - (pixelCrop.y + pixelCrop.height)\n        startCropX = pixelCrop.x + pixelCrop.width\n        startCropY = pixelCrop.y\n      } else if (ord === 'nw' || ord === 'n') {\n        fromCornerX = relativeX - pixelCrop.x\n        fromCornerY = relativeY - pixelCrop.y\n        startCropX = pixelCrop.x + pixelCrop.width\n        startCropY = pixelCrop.y + pixelCrop.height\n      }\n\n      startClientX = startCropX + box.x + fromCornerX\n      startClientY = startCropY + box.y + fromCornerY\n    }\n\n    dragData.current = {\n      startClientX,\n      startClientY,\n      startCropX,\n      startCropY,\n      clientX: e.clientX,\n      clientY: e.clientY,\n      isResize,\n      ord,\n    }\n    mouseDownOnCrop.current = true\n    setCropIsActive(true)\n  }\n\n  function onMediaPointerDown(e: PointerEvent<HTMLDivElement>) {\n    const box = getBox()\n    if (disabled || locked || (keepSelection && crop)) {\n      return\n    }\n\n    if (e.cancelable) e.preventDefault()\n    capture(e)\n    componentRef.current?.focus({ preventScroll: true })\n\n    const cropX = toLocalX(box, e.clientX)\n    const cropY = toLocalY(box, e.clientY)\n    const nextCrop: PixelCrop = {\n      unit: 'px',\n      x: cropX,\n      y: cropY,\n      width: 0,\n      height: 0,\n    }\n\n    dragData.current = {\n      startClientX: e.clientX,\n      startClientY: e.clientY,\n      startCropX: cropX,\n      startCropY: cropY,\n      clientX: e.clientX,\n      clientY: e.clientY,\n      isResize: true,\n    }\n    mouseDownOnCrop.current = true\n\n    onChange(\n      convertToPixelCrop(nextCrop, box.width, box.height),\n      convertToPercentCrop(nextCrop, box.width, box.height),\n    )\n    setCropIsActive(true)\n    setNewCropIsBeingDrawn(true)\n  }\n\n  function onPointerMove(e: PointerEvent<HTMLDivElement>) {\n    if (\n      disabled ||\n      !crop ||\n      !mouseDownOnCrop.current ||\n      e.pointerId !== pointerId.current\n    ) {\n      return\n    }\n    if (e.cancelable) e.preventDefault()\n\n    if (!dragStarted.current) {\n      dragStarted.current = true\n      onDragStart?.(e)\n    }\n\n    const data = dragData.current\n    data.clientX = e.clientX\n    data.clientY = e.clientY\n\n    const box = getBox()\n    const nextCrop = data.isResize ? resizeCrop(box) : dragCrop(box)\n\n    if (!areCropsEqual(crop, nextCrop)) {\n      onChange(\n        convertToPixelCrop(nextCrop, box.width, box.height),\n        convertToPercentCrop(nextCrop, box.width, box.height),\n      )\n    }\n  }\n\n  function onPointerDone(e: PointerEvent<HTMLDivElement>) {\n    release()\n    if (disabled || !crop || !mouseDownOnCrop.current) {\n      return\n    }\n    mouseDownOnCrop.current = false\n    dragStarted.current = false\n\n    onDragEnd?.(e)\n    const box = getBox()\n    onComplete?.(\n      convertToPixelCrop(crop, box.width, box.height),\n      convertToPercentCrop(crop, box.width, box.height),\n    )\n    setCropIsActive(false)\n    setNewCropIsBeingDrawn(false)\n  }\n\n  function dragCrop(box: Rect) {\n    const data = dragData.current\n    const nextCrop = makePixelCrop(box)\n    const xDiff = toLocalDistanceX(box, data.clientX - data.startClientX)\n    const yDiff = toLocalDistanceY(box, data.clientY - data.startClientY)\n\n    nextCrop.x = clamp(data.startCropX + xDiff, 0, box.width - nextCrop.width)\n    nextCrop.y = clamp(data.startCropY + yDiff, 0, box.height - nextCrop.height)\n\n    return nextCrop\n  }\n\n  function getPointRegion(\n    box: Rect,\n    origOrd: Ords | undefined,\n    minW: number,\n    minH: number,\n  ): Ords {\n    const data = dragData.current\n    const relativeX = toLocalX(box, data.clientX)\n    const relativeY = toLocalY(box, data.clientY)\n\n    const topHalf =\n      minH && origOrd\n        ? origOrd === 'nw' || origOrd === 'n' || origOrd === 'ne'\n        : relativeY < data.startCropY\n    const leftHalf =\n      minW && origOrd\n        ? origOrd === 'nw' || origOrd === 'w' || origOrd === 'sw'\n        : relativeX < data.startCropX\n\n    if (leftHalf) {\n      return topHalf ? 'nw' : 'sw'\n    }\n    return topHalf ? 'ne' : 'se'\n  }\n\n  function resolveMinDimensions(box: Rect): [number, number] {\n    const mw = Math.min(minWidth, box.width)\n    const mh = Math.min(minHeight, box.height)\n\n    if (!aspect || (!mw && !mh)) {\n      return [mw, mh]\n    }\n    if (aspect > 1) {\n      return mw ? [mw, mw / aspect] : [mh * aspect, mh]\n    }\n    return mh ? [mh * aspect, mh] : [mw, mw / aspect]\n  }\n\n  function resizeCrop(box: Rect) {\n    const data = dragData.current\n    const [minW, minH] = resolveMinDimensions(box)\n    let nextCrop = makePixelCrop(box)\n    const area = getPointRegion(box, data.ord, minW, minH)\n    const ord = data.ord ?? area\n    let xDiff = toLocalDistanceX(box, data.clientX - data.startClientX)\n    let yDiff = toLocalDistanceY(box, data.clientY - data.startClientY)\n\n    // With min dimensions set, stop the crop being dragged past the far side.\n    if ((minW && ord === 'nw') || ord === 'w' || ord === 'sw') {\n      xDiff = Math.min(xDiff, -minW)\n    }\n    if ((minH && ord === 'nw') || ord === 'n' || ord === 'ne') {\n      yDiff = Math.min(yDiff, -minH)\n    }\n\n    const tmpCrop: PixelCrop = { unit: 'px', x: 0, y: 0, width: 0, height: 0 }\n\n    if (area === 'ne') {\n      tmpCrop.x = data.startCropX\n      tmpCrop.width = xDiff\n      tmpCrop.height = aspect ? tmpCrop.width / aspect : Math.abs(yDiff)\n      tmpCrop.y = data.startCropY - tmpCrop.height\n    } else if (area === 'se') {\n      tmpCrop.x = data.startCropX\n      tmpCrop.y = data.startCropY\n      tmpCrop.width = xDiff\n      tmpCrop.height = aspect ? tmpCrop.width / aspect : yDiff\n    } else if (area === 'sw') {\n      tmpCrop.x = data.startCropX + xDiff\n      tmpCrop.y = data.startCropY\n      tmpCrop.width = Math.abs(xDiff)\n      tmpCrop.height = aspect ? tmpCrop.width / aspect : yDiff\n    } else if (area === 'nw') {\n      tmpCrop.x = data.startCropX + xDiff\n      tmpCrop.width = Math.abs(xDiff)\n      if (aspect) {\n        tmpCrop.height = tmpCrop.width / aspect\n        tmpCrop.y = data.startCropY - tmpCrop.height\n      } else {\n        tmpCrop.height = Math.abs(yDiff)\n        tmpCrop.y = data.startCropY + yDiff\n      }\n    }\n\n    const containedCrop = containCrop(\n      tmpCrop,\n      aspect,\n      area,\n      box.width,\n      box.height,\n      minW,\n      minH,\n      maxWidth,\n      maxHeight,\n    )\n\n    // Apply only the axes relevant to the dragged ordinate (aspect locks both).\n    if (aspect || xyOrds.indexOf(ord) > -1) {\n      nextCrop = containedCrop\n    } else if (xOrds.indexOf(ord) > -1) {\n      nextCrop.x = containedCrop.x\n      nextCrop.width = containedCrop.width\n    } else if (yOrds.indexOf(ord) > -1) {\n      nextCrop.y = containedCrop.y\n      nextCrop.height = containedCrop.height\n    }\n\n    nextCrop.x = clamp(nextCrop.x, 0, box.width - nextCrop.width)\n    nextCrop.y = clamp(nextCrop.y, 0, box.height - nextCrop.height)\n\n    return nextCrop\n  }\n\n  function onSelectionKeyDown(e: KeyboardEvent<HTMLDivElement>) {\n    if (disabled || !crop) {\n      return\n    }\n\n    const box = getBox()\n    const nextCrop = makePixelCrop(box)\n    const step =\n      e.metaKey || e.ctrlKey\n        ? NUDGE_STEP_LARGE\n        : e.shiftKey\n          ? NUDGE_STEP_MEDIUM\n          : NUDGE_STEP\n    let nudged = true\n\n    if (e.key === 'ArrowLeft') nextCrop.x -= step\n    else if (e.key === 'ArrowRight') nextCrop.x += step\n    else if (e.key === 'ArrowUp') nextCrop.y -= step\n    else if (e.key === 'ArrowDown') nextCrop.y += step\n    else nudged = false\n\n    if (!nudged) {\n      return\n    }\n    e.preventDefault()\n\n    nextCrop.x = clamp(nextCrop.x, 0, box.width - nextCrop.width)\n    nextCrop.y = clamp(nextCrop.y, 0, box.height - nextCrop.height)\n\n    const pixelCrop = convertToPixelCrop(nextCrop, box.width, box.height)\n    const percentCrop = convertToPercentCrop(nextCrop, box.width, box.height)\n    onChange(pixelCrop, percentCrop)\n    onComplete?.(pixelCrop, percentCrop)\n  }\n\n  function onHandleKeyDown(e: KeyboardEvent<HTMLDivElement>, ord: Ords) {\n    if (disabled || !crop) {\n      return\n    }\n    if (\n      e.key !== 'ArrowUp' &&\n      e.key !== 'ArrowDown' &&\n      e.key !== 'ArrowLeft' &&\n      e.key !== 'ArrowRight'\n    ) {\n      return\n    }\n\n    // Keep the event from bubbling to the selection's move handler.\n    e.stopPropagation()\n    e.preventDefault()\n\n    const box = getBox()\n    const offset =\n      e.metaKey || e.ctrlKey\n        ? NUDGE_STEP_LARGE\n        : e.shiftKey\n          ? NUDGE_STEP_MEDIUM\n          : NUDGE_STEP\n    const pixelCrop = convertToPixelCrop(crop, box.width, box.height)\n    const nudged = nudgeCrop(pixelCrop, e.key, offset, ord)\n    const contained = containCrop(\n      nudged,\n      aspect,\n      ord,\n      box.width,\n      box.height,\n      minWidth,\n      minHeight,\n      maxWidth,\n      maxHeight,\n    )\n\n    if (!areCropsEqual(crop, contained)) {\n      const percentCrop = convertToPercentCrop(contained, box.width, box.height)\n      onChange(contained, percentCrop)\n      onComplete?.(contained, percentCrop)\n    }\n  }\n\n  const cropStyle = crop\n    ? {\n        top: `${crop.y}${crop.unit}`,\n        left: `${crop.x}${crop.unit}`,\n        width: `${crop.width}${crop.unit}`,\n        height: `${crop.height}${crop.unit}`,\n      }\n    : undefined\n\n  // A freshly-started zero-size crop is hidden until the first drag move.\n  const invisible = !dragStarted.current && crop && !crop.width && !crop.height\n  const showHandles = !disabled && !locked && !newCropIsBeingDrawn\n\n  const renderHandle = (ord: Ords, extra?: string) => (\n    <div\n      key={ord}\n      data-ord={ord}\n      role=\"button\"\n      tabIndex={0}\n      aria-label={`Resize crop from the ${ORD_LABEL[ord]} corner`}\n      onKeyDown={(e) => onHandleKeyDown(e, ord)}\n      className={cn(\n        'absolute box-border size-3.5 rounded-full border-2 border-white bg-[#2dd4bf] shadow-sm outline-none pointer-coarse:size-6 focus:bg-[#0d9488]',\n        HANDLE_CLASSES[ord],\n        extra,\n        handleClassName,\n      )}\n    />\n  )\n\n  return (\n    <div\n      ref={componentRef}\n      {...rest}\n      tabIndex={-1}\n      className={cn(\n        'relative inline-block max-w-full select-none outline-none',\n        !disabled && 'touch-none',\n        disabled || locked ? 'cursor-default' : 'cursor-crosshair',\n        className,\n      )}\n      style={style}\n      onPointerMove={onPointerMove}\n      onPointerUp={onPointerDone}\n      onPointerCancel={onPointerDone}\n    >\n      <div\n        ref={mediaRef}\n        className=\"max-h-[inherit] overflow-hidden [&>img]:block [&>img]:max-h-[inherit] [&>img]:max-w-full [&>video]:block [&>video]:max-h-[inherit] [&>video]:max-w-full\"\n        onPointerDown={onMediaPointerDown}\n      >\n        {children}\n      </div>\n\n      {crop && !invisible && (\n        <svg\n          aria-hidden\n          className=\"pointer-events-none absolute left-0 top-0 h-[calc(100%+0.5px)] w-[calc(100%+0.5px)]\"\n          width=\"100%\"\n          height=\"100%\"\n        >\n          <defs>\n            <mask id={maskId}>\n              <rect width=\"100%\" height=\"100%\" fill=\"white\" />\n              {circularCrop ? (\n                <ellipse\n                  cx={`${crop.x + crop.width / 2}${crop.unit}`}\n                  cy={`${crop.y + crop.height / 2}${crop.unit}`}\n                  rx={`${crop.width / 2}${crop.unit}`}\n                  ry={`${crop.height / 2}${crop.unit}`}\n                  fill=\"black\"\n                />\n              ) : (\n                <rect\n                  x={`${crop.x}${crop.unit}`}\n                  y={`${crop.y}${crop.unit}`}\n                  width={`${crop.width}${crop.unit}`}\n                  height={`${crop.height}${crop.unit}`}\n                  fill=\"black\"\n                />\n              )}\n            </mask>\n          </defs>\n          <rect\n            fill=\"black\"\n            fillOpacity={0.5}\n            width=\"100%\"\n            height=\"100%\"\n            mask={`url(#${maskId})`}\n          />\n        </svg>\n      )}\n\n      {crop && !invisible && (\n        <div\n          ref={selectionRef}\n          style={cropStyle}\n          role=\"group\"\n          aria-label=\"Crop selection. Use the arrow keys to move it.\"\n          tabIndex={0}\n          onPointerDown={onCropPointerDown}\n          onKeyDown={onSelectionKeyDown}\n          className={cn(\n            'absolute box-border cursor-move border-2 border-[#2dd4bf] outline-none focus-visible:border-white',\n            disabled && 'cursor-[inherit]',\n            circularCrop && 'rounded-full',\n            selectionClassName,\n          )}\n        >\n          {showHandles && (\n            <>\n              {!aspect &&\n                EDGE_ORDS.map((ord) => (\n                  <div\n                    key={`bar-${ord}`}\n                    data-ord={ord}\n                    className={cn('absolute', BAR_CLASSES[ord])}\n                  />\n                ))}\n              {CORNER_ORDS.map((ord) => renderHandle(ord))}\n              {!aspect &&\n                EDGE_ORDS.map((ord) =>\n                  renderHandle(ord, 'pointer-coarse:hidden'),\n                )}\n            </>\n          )}\n\n          {renderSelectionAddon && (\n            <div onPointerDown={(e) => e.stopPropagation()}>\n              {renderSelectionAddon({ cropIsActive, newCropIsBeingDrawn })}\n            </div>\n          )}\n\n          {ruleOfThirds && (\n            <>\n              <div className=\"pointer-events-none absolute left-1/3 top-0 h-full w-px bg-white/40\" />\n              <div className=\"pointer-events-none absolute left-2/3 top-0 h-full w-px bg-white/40\" />\n              <div className=\"pointer-events-none absolute left-0 top-1/3 h-px w-full bg-white/40\" />\n              <div className=\"pointer-events-none absolute left-0 top-2/3 h-px w-full bg-white/40\" />\n            </>\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}