{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-zoom",
  "type": "registry:block",
  "title": "Image Zoom",
  "description": "An interactive image viewer for React apps. Easily zoom, drag, resize, and rotate images with smooth controls. Perfect for galleries, portfolios, or any UI where users need to explore images in detail.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dialog"
  ],
  "files": [
    {
      "path": "registry/new-york/image-zoom/image-zoom.tsx",
      "content": "import { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  IterationCwIcon,\n  MinusIcon,\n  PlusIcon,\n  RotateCwSquareIcon,\n} from \"lucide-react\";\nimport { motion, useMotionValue, useSpring } from \"motion/react\";\nimport Image, { type ImageProps } from \"next/image\";\nimport React from \"react\";\n\nexport const ImageZoom: React.ComponentType<ImageProps> = (imageProps) => {\n  return (\n    <Dialog>\n      <DialogTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          className=\"relative size-32 overflow-hidden\"\n          size=\"icon\"\n        >\n          <Image\n            {...imageProps}\n            alt={imageProps.alt || \"\"}\n            layout=\"fill\"\n            className=\"object-cover\"\n          />\n        </Button>\n      </DialogTrigger>\n      <DialogContent className=\"sm:max-w-[90vw]\">\n        <DialogHeader>\n          <DialogTitle>Image Zoom</DialogTitle>\n          <DialogDescription>Image Zoom Description</DialogDescription>\n        </DialogHeader>\n        <ImageZoomManipulator\n          src={imageProps.src.toString()}\n          alt={imageProps.alt || \"\"}\n        />\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const ImageZoomManipulator: React.ComponentType<\n  React.ComponentProps<typeof motion.img>\n> = (imageProps) => {\n  const [originalDragConstraints, setOriginalDragConstraints] = React.useState({\n    top: 0,\n    left: 0,\n    right: 0,\n    bottom: 0,\n  });\n\n  const [currentDragConstraints, setCurrentDragConstraints] = React.useState({\n    top: 0,\n    left: 0,\n    right: 0,\n    bottom: 0,\n  });\n\n  const containerRef = React.useRef<HTMLDivElement | null>(null);\n\n  const scale = useSpring(1, { visualDuration: 0.3, bounce: 0.35 });\n  scale.on(\"change\", (value) => {\n    setCurrentDragConstraints({\n      top: originalDragConstraints.top * value * 0.7,\n      left: originalDragConstraints.left * value * 0.7,\n      right: originalDragConstraints.right * value * 0.7,\n      bottom: originalDragConstraints.bottom * value * 0.7,\n    });\n  });\n\n  const rotate = useSpring(0, { visualDuration: 0.35, bounce: 0.2 });\n\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n\n  const [isMounted, setIsMounted] = React.useState(false);\n\n  React.useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  return (\n    <motion.div\n      ref={(ref) => {\n        if (containerRef.current) return;\n\n        containerRef.current = ref;\n\n        const dragConstraints = {\n          top: ref ? -ref.clientHeight * 0.7 : 0,\n          left: ref ? -ref.clientWidth * 0.7 : 0,\n          right: ref ? ref.clientWidth * 0.7 : 0,\n          bottom: ref ? ref.clientHeight * 0.7 : 0,\n        };\n\n        setCurrentDragConstraints(dragConstraints);\n        setOriginalDragConstraints(dragConstraints);\n      }}\n      className=\"relative h-[70vh] w-full overflow-hidden rounded-xl border\"\n    >\n      <motion.img\n        {...imageProps}\n        className=\"size-full cursor-grab object-contain\"\n        drag={isMounted}\n        dragMomentum={false}\n        _dragX={x}\n        _dragY={y}\n        whileDrag={{ cursor: \"grabbing\" }}\n        dragConstraints={currentDragConstraints}\n        style={{ scale, rotate, x, y }}\n      />\n\n      <div className=\"absolute top-2 right-2\">\n        <Button\n          variant=\"secondary\"\n          size=\"icon\"\n          className=\"rounded-r-none\"\n          onClick={() => {\n            scale.set(scale.get() + 1);\n          }}\n        >\n          <PlusIcon />\n        </Button>\n        <Button\n          variant=\"secondary\"\n          size=\"icon\"\n          className=\"rounded-l-none rounded-r-none border-l-0\"\n          onClick={() => {\n            scale.set(Math.max(0.01, scale.get() - 1));\n          }}\n        >\n          <MinusIcon />\n        </Button>\n        <Button\n          variant=\"secondary\"\n          size=\"icon\"\n          className=\"rounded-l-none rounded-r-none border-l-0\"\n          onClick={() => {\n            const current = rotate.get();\n            // When rotating, we want the angle to always be a multiple of 90 (0, 90, 180, 270, ...).\n            // Math explanation:\n            // - Math.round(current / 90) * 90 snaps the current angle to the nearest multiple of 90.\n            // - Then we add 90 to rotate clockwise to the next step.\n            // This way, even if the angle is a bit off (like 91), it will always snap to 90, 180, 270, etc.\n            const next = Math.round(current / 90) * 90 + 90;\n            rotate.set(next);\n          }}\n        >\n          <RotateCwSquareIcon />\n        </Button>\n        <Button\n          variant=\"secondary\"\n          size=\"icon\"\n          className=\"rounded-l-none border-l-0\"\n          onClick={() => {\n            scale.jump(1);\n            rotate.jump(0);\n            x.jump(0);\n            y.jump(0);\n            // setResetKey((prev) => prev + 1);\n          }}\n        >\n          <IterationCwIcon />\n        </Button>\n      </div>\n    </motion.div>\n  );\n};\n",
      "type": "registry:component"
    }
  ]
}