{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "view-magnifier",
  "type": "registry:ui",
  "title": "View Magnifier",
  "description": "A view magnifier component for Next.js apps with next-themes and Tailwind CSS, supporting system, light, and dark modes.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/bucharitesh/view-magnifier.tsx",
      "content": "'use client';\n\nimport { cn } from '@/lib/utils';\nimport { animate, motion, useMotionValue, useTransform } from 'motion/react';\nimport type React from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface ViewMagnifierProps {\n  children: React.ReactNode;\n  className?: string;\n  maxScale?: number;\n  onScaleChange?: (isActive: boolean) => void;\n  onMaxScaleReached?: (isAtMax: boolean) => void;\n}\n\nconst ViewMagnifier: React.FC<ViewMagnifierProps> = ({\n  className,\n  children,\n  maxScale = 1.8,\n  onScaleChange,\n  onMaxScaleReached,\n  ...props\n}) => {\n  const [isMouseDown, setIsMouseDown] = useState<boolean>(false);\n  const [zoomLevel, setZoomLevel] = useState<number>(100);\n  const [isAtMaxScale, setIsAtMaxScale] = useState<boolean>(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const startX = useRef<number>(0);\n  const initialScale = useRef<number>(1);\n  const scale = useMotionValue(1);\n  const opacity = useTransform(scale, [1, maxScale], [0, 1]);\n  const containerScale = useTransform(scale, [1, maxScale], [1, 1.6]);\n\n  // Monitor scale changes for max scale callback\n  useEffect(() => {\n    const unsubscribe = scale.on('change', (latestScale) => {\n      const newIsAtMaxScale = Math.abs(latestScale - maxScale) < 0.01;\n      if (newIsAtMaxScale !== isAtMaxScale) {\n        setIsAtMaxScale(newIsAtMaxScale);\n        onMaxScaleReached?.(newIsAtMaxScale);\n      }\n    });\n\n    return () => unsubscribe();\n  }, [scale, maxScale, isAtMaxScale, onMaxScaleReached]);\n\n  const handleZoomAnimation = useCallback(\n    (targetScale: number) => {\n      animate(scale, targetScale, {\n        type: 'spring',\n        stiffness: 400,\n        damping: 30,\n        onUpdate: (latest) => setZoomLevel(Math.round(latest * 100)),\n      });\n    },\n    [scale]\n  );\n\n  const handlePointerDown = useCallback(\n    (e: React.PointerEvent<HTMLButtonElement>): void => {\n      setIsMouseDown(true);\n      startX.current = e.clientX;\n      initialScale.current = scale.get();\n      e.currentTarget.setPointerCapture(e.pointerId);\n      onScaleChange?.(true);\n    },\n    [scale, onScaleChange]\n  );\n\n  const handlePointerUp = useCallback(\n    (e: React.PointerEvent<HTMLButtonElement>): void => {\n      if (isMouseDown) {\n        setIsMouseDown(false);\n        handleZoomAnimation(1);\n        e.currentTarget.releasePointerCapture(e.pointerId);\n        onScaleChange?.(false);\n      }\n    },\n    [isMouseDown, handleZoomAnimation, onScaleChange]\n  );\n\n  const handlePointerMove = useCallback(\n    (e: React.PointerEvent<HTMLButtonElement>): void => {\n      if (!isMouseDown) return;\n\n      const deltaX = e.clientX - startX.current;\n      const scaleChange = deltaX * 0.005;\n      const newScale = Math.max(\n        0.8,\n        Math.min(maxScale, initialScale.current + scaleChange)\n      );\n\n      scale.set(newScale);\n      setZoomLevel(Math.round(newScale * 100));\n    },\n    [isMouseDown, maxScale, scale]\n  );\n\n  return (\n    <div ref={containerRef} className=\"outline-hidden\" {...props}>\n      <motion.div\n        className={cn(\n          'pointer-events-none fixed inset-0 h-screen w-screen outline-hidden backdrop-blur-xl z-[9999]',\n          'after:inset-0 after:h-full after:w-full after:rounded-[inherit] after:content-[\"\"]',\n          'after:pointer-events-none after:absolute dark:after:block',\n          'dark:after:shadow-[inset_0_0_0_1px_hsla(0,0%,100%,0.2)]'\n        )}\n        style={{ opacity }}\n        aria-hidden=\"true\"\n      />\n\n      <motion.div\n        className={cn(\n          'relative right-1/2 left-1/2 my-3 h-auto w-full overflow-visible z-[10000]',\n          'rounded-2xl',\n          'transform lg:transform-none',\n          className\n        )}\n        style={{\n          scale: containerScale,\n          translateX: '-50%',\n          translateZ: '0px',\n        }}\n        role=\"img\"\n        aria-label={`Content at zoom level ${zoomLevel}%`}\n      >\n        <div className=\"relative h-full w-full overflow-hidden rounded-2xl\">\n          {children}\n        </div>\n\n        <motion.div\n          style={{ opacity }}\n          className=\"absolute inset-0 h-full w-full rounded-[inherit] shadow-[0px_1px_1px_0px_rgba(0,0,0,0.02),0px_16px_24px_-4px_rgba(0,0,0,0.04),0px_32px_48px_-8px_rgba(0,0,0,0.06)]\"\n          aria-hidden=\"true\"\n        />\n\n        <motion.button\n          onPointerDown={handlePointerDown}\n          onPointerUp={handlePointerUp}\n          onPointerMove={handlePointerMove}\n          onPointerLeave={handlePointerUp}\n          style={{\n            scale: containerScale,\n            translateY: '-50%',\n            translateZ: '0px',\n          }}\n          aria-label={`Drag to zoom. Current zoom level: ${zoomLevel}%`}\n          aria-valuemin={80}\n          aria-valuemax={180}\n          aria-valuenow={zoomLevel}\n          role=\"slider\"\n          className={cn(\n            '-right-6 absolute top-1/2',\n            'h-14 w-1 rounded-full',\n            'bg-gray-400 dark:bg-gray-600',\n            'hover:bg-gray-500 dark:hover:bg-gray-500',\n            'transition-colors duration-300',\n            'focus-visible:outline-hidden focus-visible:ring-2',\n            'focus-visible:ring-gray-400 dark:focus-visible:ring-gray-500',\n            'focus-visible:ring-offset-2',\n            'focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',\n            'hidden md:block',\n            isMouseDown ? 'cursor-grabbing' : 'cursor-grab',\n            'after:-left-2 after:absolute after:top-0 after:h-full after:w-4 after:content-[\"\"]'\n          )}\n          touch-action=\"none\"\n        />\n      </motion.div>\n    </div>\n  );\n};\n\nexport default ViewMagnifier;\n",
      "type": "registry:ui"
    }
  ]
}