{
  "name": "image-comparison",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "ui/image-comparison.tsx",
      "content": "import { GripHorizontal, GripVertical } from \"lucide-react\";\nimport { motion, useMotionValue, useTransform } from \"motion/react\";\nimport type React from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// Basic Comparison Slider\ninterface ImageComparisonProps {\n  beforeImage: string;\n  afterImage: string;\n  beforeLabel?: string;\n  afterLabel?: string;\n  className?: string;\n  initialPosition?: number;\n  orientation?: \"horizontal\" | \"vertical\";\n  showLabels?: boolean;\n  sliderColor?: string;\n}\n\nexport function ImageComparison({\n  beforeImage,\n  afterImage,\n  beforeLabel = \"Before\",\n  afterLabel = \"After\",\n  className,\n  initialPosition = 50,\n  orientation = \"horizontal\",\n  showLabels = true,\n  sliderColor = \"hsl(var(--background))\",\n}: ImageComparisonProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [position, setPosition] = useState(initialPosition);\n  const [isDragging, setIsDragging] = useState(false);\n\n  const handleMove = useCallback(\n    (clientX: number, clientY: number) => {\n      if (!containerRef.current) return;\n\n      const rect = containerRef.current.getBoundingClientRect();\n      let newPosition: number;\n\n      if (orientation === \"horizontal\") {\n        newPosition = ((clientX - rect.left) / rect.width) * 100;\n      } else {\n        newPosition = ((clientY - rect.top) / rect.height) * 100;\n      }\n\n      setPosition(Math.max(0, Math.min(100, newPosition)));\n    },\n    [orientation],\n  );\n\n  const handleMouseDown = (e: React.MouseEvent) => {\n    e.preventDefault();\n    setIsDragging(true);\n  };\n\n  const handleTouchStart = () => {\n    setIsDragging(true);\n  };\n\n  useEffect(() => {\n    const handleMouseMove = (e: MouseEvent) => {\n      if (isDragging) {\n        handleMove(e.clientX, e.clientY);\n      }\n    };\n\n    const handleTouchMove = (e: TouchEvent) => {\n      if (isDragging && e.touches[0]) {\n        handleMove(e.touches[0].clientX, e.touches[0].clientY);\n      }\n    };\n\n    const handleEnd = () => {\n      setIsDragging(false);\n    };\n\n    if (isDragging) {\n      window.addEventListener(\"mousemove\", handleMouseMove);\n      window.addEventListener(\"mouseup\", handleEnd);\n      window.addEventListener(\"touchmove\", handleTouchMove);\n      window.addEventListener(\"touchend\", handleEnd);\n    }\n\n    return () => {\n      window.removeEventListener(\"mousemove\", handleMouseMove);\n      window.removeEventListener(\"mouseup\", handleEnd);\n      window.removeEventListener(\"touchmove\", handleTouchMove);\n      window.removeEventListener(\"touchend\", handleEnd);\n    };\n  }, [isDragging, handleMove]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        \"relative select-none overflow-hidden rounded-xl\",\n        className,\n      )}\n    >\n      {/* After Image (Background) */}\n      {/* biome-ignore lint/performance/noImgElement: next/image causes ESM issues with fumadocs-mdx */}\n      <img\n        src={afterImage}\n        alt={afterLabel}\n        className=\"h-full w-full object-cover\"\n        draggable={false}\n      />\n\n      {/* Before Image (Clipped) */}\n      <div\n        className=\"absolute inset-0 overflow-hidden\"\n        style={{\n          clipPath:\n            orientation === \"horizontal\"\n              ? `inset(0 ${100 - position}% 0 0)`\n              : `inset(0 0 ${100 - position}% 0)`,\n        }}\n      >\n        {/* biome-ignore lint/performance/noImgElement: next/image causes ESM issues with fumadocs-mdx */}\n        <img\n          src={beforeImage}\n          alt={beforeLabel}\n          className=\"h-full w-full object-cover\"\n          draggable={false}\n        />\n      </div>\n\n      {/* Slider Line */}\n      <div\n        className={cn(\n          \"absolute z-10\",\n          orientation === \"horizontal\"\n            ? \"top-0 h-full w-0.5 -translate-x-1/2\"\n            : \"left-0 h-0.5 w-full -translate-y-1/2\",\n        )}\n        style={{\n          [orientation === \"horizontal\" ? \"left\" : \"top\"]: `${position}%`,\n          backgroundColor: sliderColor,\n          boxShadow: \"0 0 10px rgba(0,0,0,0.3)\",\n        }}\n      />\n\n      {/* Slider Handle */}\n      <motion.div\n        className={cn(\n          \"absolute z-20 flex cursor-grab items-center justify-center rounded-full border-2 bg-background shadow-lg active:cursor-grabbing\",\n          orientation === \"horizontal\"\n            ? \"h-10 w-10 -translate-x-1/2 -translate-y-1/2\"\n            : \"h-10 w-10 -translate-x-1/2 -translate-y-1/2\",\n        )}\n        style={{\n          [orientation === \"horizontal\" ? \"left\" : \"left\"]:\n            orientation === \"horizontal\" ? `${position}%` : \"50%\",\n          [orientation === \"horizontal\" ? \"top\" : \"top\"]:\n            orientation === \"horizontal\" ? \"50%\" : `${position}%`,\n          borderColor: sliderColor,\n        }}\n        onMouseDown={handleMouseDown}\n        onTouchStart={handleTouchStart}\n        whileHover={{ scale: 1.1 }}\n        whileTap={{ scale: 0.95 }}\n      >\n        {orientation === \"horizontal\" ? (\n          <GripVertical className=\"h-5 w-5 text-muted-foreground\" />\n        ) : (\n          <GripHorizontal className=\"h-5 w-5 text-muted-foreground\" />\n        )}\n      </motion.div>\n\n      {/* Labels */}\n      {showLabels && (\n        <>\n          <div\n            className={cn(\n              \"absolute rounded-md bg-background/80 px-2 py-1 font-medium text-xs backdrop-blur-sm\",\n              orientation === \"horizontal\" ? \"top-3 left-3\" : \"top-3 left-3\",\n            )}\n          >\n            {beforeLabel}\n          </div>\n          <div\n            className={cn(\n              \"absolute rounded-md bg-background/80 px-2 py-1 font-medium text-xs backdrop-blur-sm\",\n              orientation === \"horizontal\"\n                ? \"top-3 right-3\"\n                : \"bottom-3 left-3\",\n            )}\n          >\n            {afterLabel}\n          </div>\n        </>\n      )}\n    </div>\n  );\n}\n\n// Hover Reveal Comparison\ninterface ImageComparisonHoverProps {\n  beforeImage: string;\n  afterImage: string;\n  beforeLabel?: string;\n  afterLabel?: string;\n  className?: string;\n  showLabels?: boolean;\n}\n\nexport function ImageComparisonHover({\n  beforeImage,\n  afterImage,\n  beforeLabel = \"Before\",\n  afterLabel = \"After\",\n  className,\n  showLabels = true,\n}: ImageComparisonHoverProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [position, setPosition] = useState(50);\n\n  const handleMouseMove = (e: React.MouseEvent) => {\n    if (!containerRef.current) return;\n    const rect = containerRef.current.getBoundingClientRect();\n    const x = ((e.clientX - rect.left) / rect.width) * 100;\n    setPosition(Math.max(0, Math.min(100, x)));\n  };\n\n  const handleMouseLeave = () => {\n    setPosition(50);\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        \"relative cursor-ew-resize select-none overflow-hidden rounded-xl\",\n        className,\n      )}\n      onMouseMove={handleMouseMove}\n      onMouseLeave={handleMouseLeave}\n      role=\"button\"\n      tabIndex={0}\n    >\n      {/* After Image */}\n      {/* biome-ignore lint/performance/noImgElement: next/image causes ESM issues with fumadocs-mdx */}\n      <img\n        src={afterImage}\n        alt={afterLabel}\n        className=\"h-full w-full object-cover\"\n        draggable={false}\n      />\n\n      {/* Before Image */}\n      <motion.div\n        className=\"absolute inset-0 overflow-hidden\"\n        animate={{ clipPath: `inset(0 ${100 - position}% 0 0)` }}\n        transition={{ type: \"tween\", duration: 0.1 }}\n      >\n        {/* biome-ignore lint/performance/noImgElement: next/image causes ESM issues with fumadocs-mdx */}\n        <img\n          src={beforeImage}\n          alt={beforeLabel}\n          className=\"h-full w-full object-cover\"\n          draggable={false}\n        />\n      </motion.div>\n\n      {/* Divider Line */}\n      {/* <motion.div\n        className=\"absolute top-0 h-full w-0.5 bg-background\"\n        animate={{ left: `${position}%` }}\n        transition={{ type: \"tween\", duration: 0.1 }}\n        style={{ transform: \"translateX(-50%)\", boxShadow: \"0 0 10px rgba(0,0,0,0.3)\" }}\n      /> */}\n\n      {/* Labels */}\n      {showLabels && (\n        <>\n          <div className=\"absolute top-3 left-3 rounded-md bg-background/80 px-2 py-1 font-medium text-xs backdrop-blur-sm\">\n            {beforeLabel}\n          </div>\n          <div className=\"absolute top-3 right-3 rounded-md bg-background/80 px-2 py-1 font-medium text-xs backdrop-blur-sm\">\n            {afterLabel}\n          </div>\n        </>\n      )}\n    </div>\n  );\n}\n\n// Split View Comparison\ninterface ImageComparisonSplitProps {\n  beforeImage: string;\n  afterImage: string;\n  beforeLabel?: string;\n  afterLabel?: string;\n  className?: string;\n  gap?: number;\n}\n\nexport function ImageComparisonSplit({\n  beforeImage,\n  afterImage,\n  beforeLabel = \"Before\",\n  afterLabel = \"After\",\n  className,\n  gap = 4,\n}: ImageComparisonSplitProps) {\n  return (\n    <div\n      className={cn(\"flex overflow-hidden rounded-xl\", className)}\n      style={{ gap }}\n    >\n      <div className=\"relative flex-1 overflow-hidden\">\n        {/* biome-ignore lint/performance/noImgElement: next/image causes ESM issues with fumadocs-mdx */}\n        <img\n          src={beforeImage}\n          alt={beforeLabel}\n          className=\"h-full w-full object-cover\"\n          draggable={false}\n        />\n        <div className=\"absolute bottom-3 left-3 rounded-md bg-background/80 px-2 py-1 font-medium text-xs backdrop-blur-sm\">\n          {beforeLabel}\n        </div>\n      </div>\n      <div className=\"relative flex-1 overflow-hidden\">\n        {/* biome-ignore lint/performance/noImgElement: next/image causes ESM issues with fumadocs-mdx */}\n        <img\n          src={afterImage}\n          alt={afterLabel}\n          className=\"h-full w-full object-cover\"\n          draggable={false}\n        />\n        <div className=\"absolute right-3 bottom-3 rounded-md bg-background/80 px-2 py-1 font-medium text-xs backdrop-blur-sm\">\n          {afterLabel}\n        </div>\n      </div>\n    </div>\n  );\n}\n\n// Fade Toggle Comparison\ninterface ImageComparisonFadeProps {\n  beforeImage: string;\n  afterImage: string;\n  beforeLabel?: string;\n  afterLabel?: string;\n  className?: string;\n  showLabels?: boolean;\n}\n\nexport function ImageComparisonFade({\n  beforeImage,\n  afterImage,\n  beforeLabel = \"Before\",\n  afterLabel = \"After\",\n  className,\n  showLabels = true,\n}: ImageComparisonFadeProps) {\n  const [showBefore, setShowBefore] = useState(true);\n\n  return (\n    <div\n      className={cn(\n        \"group relative cursor-pointer select-none overflow-hidden rounded-xl\",\n        className,\n      )}\n      onClick={() => setShowBefore(!showBefore)}\n      role=\"button\"\n      tabIndex={0}\n    >\n      {/* After Image */}\n      {/* biome-ignore lint/performance/noImgElement: next/image causes ESM issues with fumadocs-mdx */}\n      <img\n        src={afterImage}\n        alt={afterLabel}\n        className=\"h-full w-full object-cover\"\n        draggable={false}\n      />\n\n      {/* Before Image with fade */}\n      <motion.div\n        className=\"absolute inset-0\"\n        animate={{ opacity: showBefore ? 1 : 0 }}\n        transition={{ duration: 0.5 }}\n      >\n        {/* biome-ignore lint/performance/noImgElement: next/image causes ESM issues with fumadocs-mdx */}\n        <img\n          src={beforeImage}\n          alt={beforeLabel}\n          className=\"h-full w-full object-cover\"\n          draggable={false}\n        />\n      </motion.div>\n\n      {/* Label */}\n      {showLabels && (\n        <motion.div\n          className=\"absolute top-3 left-1/2 -translate-x-1/2 rounded-md bg-background/80 px-3 py-1.5 font-medium text-sm backdrop-blur-sm\"\n          key={showBefore ? \"before\" : \"after\"}\n          initial={{ opacity: 0, y: -10 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.2 }}\n        >\n          {showBefore ? beforeLabel : afterLabel}\n        </motion.div>\n      )}\n\n      {/* Click hint */}\n      <div className=\"absolute bottom-3 left-1/2 -translate-x-1/2 rounded-md bg-background/80 px-2 py-1 text-muted-foreground text-xs opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100\">\n        Click to toggle\n      </div>\n    </div>\n  );\n}\n\n// Swipe Comparison\ninterface ImageComparisonSwipeProps {\n  beforeImage: string;\n  afterImage: string;\n  beforeLabel?: string;\n  afterLabel?: string;\n  className?: string;\n}\n\nexport function ImageComparisonSwipe({\n  beforeImage,\n  afterImage,\n  beforeLabel = \"Before\",\n  afterLabel = \"After\",\n  className,\n}: ImageComparisonSwipeProps) {\n  const x = useMotionValue(0);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [containerWidth, setContainerWidth] = useState(0);\n\n  useEffect(() => {\n    if (containerRef.current) {\n      setContainerWidth(containerRef.current.offsetWidth);\n    }\n  }, []);\n\n  const clipPath = useTransform(\n    x,\n    [-containerWidth / 2, containerWidth / 2],\n    [0, 100],\n  );\n  const displayClipPath = useTransform(\n    clipPath,\n    (v) => `inset(0 ${100 - (50 + v / 2)}% 0 0)`,\n  );\n  const linePosition = useTransform(clipPath, (v) => `${50 + v / 2}%`);\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        \"relative select-none overflow-hidden rounded-xl\",\n        className,\n      )}\n    >\n      {/* After Image */}\n      {/* biome-ignore lint/performance/noImgElement: next/image causes ESM issues with fumadocs-mdx */}\n      <img\n        src={afterImage}\n        alt={afterLabel}\n        className=\"h-full w-full object-cover\"\n        draggable={false}\n      />\n\n      {/* Before Image */}\n      <motion.div\n        className=\"absolute inset-0 overflow-hidden\"\n        style={{ clipPath: displayClipPath }}\n      >\n        {/* biome-ignore lint/performance/noImgElement: next/image causes ESM issues with fumadocs-mdx */}\n        <img\n          src={beforeImage}\n          alt={beforeLabel}\n          className=\"h-full w-full object-cover\"\n          draggable={false}\n        />\n      </motion.div>\n\n      {/* Slider Line */}\n      <motion.div\n        className=\"absolute top-0 h-full w-0.5 bg-background\"\n        style={{\n          left: linePosition,\n          transform: \"translateX(-50%)\",\n          boxShadow: \"0 0 10px rgba(0,0,0,0.3)\",\n        }}\n      />\n\n      {/* Draggable Handle */}\n      <motion.div\n        className=\"absolute top-1/2 left-1/2 z-20 flex h-12 w-12 -translate-y-1/2 cursor-grab items-center justify-center rounded-full border-2 border-background bg-background shadow-lg active:cursor-grabbing\"\n        drag=\"x\"\n        dragConstraints={{\n          left: -containerWidth / 2 + 20,\n          right: containerWidth / 2 - 20,\n        }}\n        dragElastic={0}\n        style={{ x }}\n        whileHover={{ scale: 1.1 }}\n        whileTap={{ scale: 0.95 }}\n      >\n        <GripVertical className=\"h-5 w-5 text-muted-foreground\" />\n      </motion.div>\n\n      {/* Labels */}\n      <div className=\"absolute top-3 left-3 rounded-md bg-background/80 px-2 py-1 font-medium text-xs backdrop-blur-sm\">\n        {beforeLabel}\n      </div>\n      <div className=\"absolute top-3 right-3 rounded-md bg-background/80 px-2 py-1 font-medium text-xs backdrop-blur-sm\">\n        {afterLabel}\n      </div>\n    </div>\n  );\n}\n\n// Lens Comparison (magnifying glass effect)\ninterface ImageComparisonLensProps {\n  beforeImage: string;\n  afterImage: string;\n  className?: string;\n  lensSize?: number;\n}\n\nexport function ImageComparisonLens({\n  beforeImage,\n  afterImage,\n  className,\n  lensSize = 150,\n}: ImageComparisonLensProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [lensPosition, setLensPosition] = useState({ x: 50, y: 50 });\n  const [isHovering, setIsHovering] = useState(false);\n\n  const handleMouseMove = (e: React.MouseEvent) => {\n    if (!containerRef.current) return;\n    const rect = containerRef.current.getBoundingClientRect();\n    const x = ((e.clientX - rect.left) / rect.width) * 100;\n    const y = ((e.clientY - rect.top) / rect.height) * 100;\n    setLensPosition({ x, y });\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        \"relative cursor-none select-none overflow-hidden rounded-xl\",\n        className,\n      )}\n      onMouseMove={handleMouseMove}\n      onMouseEnter={() => setIsHovering(true)}\n      onMouseLeave={() => setIsHovering(false)}\n      role=\"button\"\n      tabIndex={0}\n    >\n      {/* Before Image (Background) */}\n      {/* biome-ignore lint/performance/noImgElement: next/image causes ESM issues with fumadocs-mdx */}\n      <img\n        src={beforeImage}\n        alt=\"Before\"\n        className=\"h-full w-full object-cover\"\n        draggable={false}\n      />\n\n      {/* Lens showing After Image */}\n      <motion.div\n        className=\"pointer-events-none absolute overflow-hidden rounded-full border-2 border-background shadow-2xl\"\n        style={{\n          width: lensSize,\n          height: lensSize,\n          left: `calc(${lensPosition.x}% - ${lensSize / 2}px)`,\n          top: `calc(${lensPosition.y}% - ${lensSize / 2}px)`,\n        }}\n        animate={{ opacity: isHovering ? 1 : 0, scale: isHovering ? 1 : 0.8 }}\n        transition={{ duration: 0.2 }}\n      >\n        <div\n          className=\"h-full w-full\"\n          style={{\n            backgroundImage: `url(${afterImage})`,\n            backgroundSize: containerRef.current\n              ? `${containerRef.current.offsetWidth}px ${containerRef.current.offsetHeight}px`\n              : \"100% 100%\",\n            backgroundPosition: `${lensPosition.x}% ${lensPosition.y}%`,\n          }}\n        />\n      </motion.div>\n\n      {/* Labels */}\n      <div className=\"absolute top-3 left-3 rounded-md bg-background/80 px-2 py-1 font-medium text-xs backdrop-blur-sm\">\n        Before\n      </div>\n      <div className=\"absolute top-3 right-3 rounded-md bg-background/80 px-2 py-1 font-medium text-xs backdrop-blur-sm\">\n        Hover to see After\n      </div>\n    </div>\n  );\n}\n\nexport type {\n  ImageComparisonFadeProps,\n  ImageComparisonHoverProps,\n  ImageComparisonLensProps,\n  ImageComparisonProps,\n  ImageComparisonSplitProps,\n  ImageComparisonSwipeProps,\n};\n",
      "type": "registry:ui",
      "target": ""
    }
  ],
  "type": "registry:ui"
}