{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-compare",
  "title": "Image Compare",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/image-compare/ImageCompare.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { MoveHorizontal, MoveVertical } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { imageCompareVariants, type ImageCompareVariants } from './image-compare.variants'\n\nexport interface ImageCompareProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>, ImageCompareVariants {\n  beforeSrc: string\n  afterSrc: string\n  beforeAlt?: string\n  afterAlt?: string\n  beforeLabel?: string\n  afterLabel?: string\n  /** Controlled slider position (0–100). */\n  value?: number\n  /** Uncontrolled initial position (0–100). */\n  defaultValue?: number\n  /** Fired with the new position (0–100) on drag / keyboard move. */\n  onValueChange?: (value: number) => void\n  disabled?: boolean\n  showLabels?: boolean\n  showHandle?: boolean\n  /** Custom handle content — replaces the default arrow icon. */\n  handle?: React.ReactNode\n}\n\nfunction clamp(v: number): number {\n  return Math.min(100, Math.max(0, v))\n}\n\nconst ImageCompare = React.forwardRef<HTMLDivElement, ImageCompareProps>(\n  (\n    {\n      beforeSrc,\n      afterSrc,\n      beforeAlt = 'Before',\n      afterAlt = 'After',\n      beforeLabel = 'Before',\n      afterLabel = 'After',\n      value: controlledValue,\n      defaultValue = 50,\n      onValueChange,\n      orientation = 'horizontal',\n      disabled = false,\n      showLabels = true,\n      showHandle = true,\n      handle,\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const isControlled = controlledValue !== undefined\n    const [internal, setInternal] = React.useState(defaultValue)\n    const position = isControlled ? controlledValue! : internal\n\n    const containerRef = React.useRef<HTMLDivElement | null>(null)\n    const draggingRef = React.useRef(false)\n\n    const setRefs = React.useCallback(\n      (node: HTMLDivElement | null) => {\n        containerRef.current = node\n        if (typeof ref === 'function') ref(node)\n        else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n      },\n      [ref],\n    )\n\n    function setPosition(next: number) {\n      const clamped = clamp(next)\n      if (!isControlled) setInternal(clamped)\n      onValueChange?.(clamped)\n    }\n\n    function updateFromPointer(clientX: number, clientY: number) {\n      const el = containerRef.current\n      if (!el) return\n      const rect = el.getBoundingClientRect()\n      if (orientation === 'horizontal') {\n        setPosition(((clientX - rect.left) / rect.width) * 100)\n      } else {\n        setPosition(((clientY - rect.top) / rect.height) * 100)\n      }\n    }\n\n    function onPointerDown(e: React.PointerEvent) {\n      if (disabled) return\n      draggingRef.current = true\n      const el = e.currentTarget as HTMLElement\n      try {\n        el.setPointerCapture(e.pointerId)\n      } catch {\n        // pointer capture can fail on synthetic / non-active pointers\n      }\n      updateFromPointer(e.clientX, e.clientY)\n    }\n\n    function onPointerMove(e: React.PointerEvent) {\n      if (!draggingRef.current || disabled) return\n      updateFromPointer(e.clientX, e.clientY)\n    }\n\n    function onPointerUp(e: React.PointerEvent) {\n      if (!draggingRef.current) return\n      draggingRef.current = false\n      const el = e.currentTarget as HTMLElement\n      try {\n        el.releasePointerCapture(e.pointerId)\n      } catch {\n        // pointer already released\n      }\n    }\n\n    // Keyboard support: arrow keys move the slider by 1% (Shift = 10%)\n    function onKeyDown(e: React.KeyboardEvent) {\n      if (disabled) return\n      const isH = orientation === 'horizontal'\n      const step = e.shiftKey ? 10 : 1\n      let next = position\n      if (isH) {\n        if (e.key === 'ArrowLeft') next -= step\n        else if (e.key === 'ArrowRight') next += step\n        else return\n      } else {\n        if (e.key === 'ArrowUp') next -= step\n        else if (e.key === 'ArrowDown') next += step\n        else return\n      }\n      e.preventDefault()\n      setPosition(next)\n    }\n\n    // \"After\" image is clipped to show only the right portion.\n    // Dragging right (higher %) reveals more of \"before\" on the left.\n    const pct = position\n    const clipStyle =\n      orientation === 'horizontal' ? { clipPath: `inset(0 0 0 ${pct}%)` } : { clipPath: `inset(${pct}% 0 0 0)` }\n    const dividerStyle = orientation === 'horizontal' ? { left: `${pct}%` } : { top: `${pct}%` }\n\n    React.useEffect(() => {\n      return () => {\n        draggingRef.current = false\n      }\n    }, [])\n\n    return (\n      <div\n        ref={setRefs}\n        data-uipkge=\"\"\n        data-slot=\"image-compare\"\n        data-orientation={orientation}\n        data-disabled={disabled ? '' : undefined}\n        className={cn(imageCompareVariants({ orientation }), className)}\n        style={{ touchAction: 'none' }}\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={onPointerUp}\n        onPointerCancel={onPointerUp}\n        {...props}\n      >\n        {/* Before (base layer, full) */}\n        <img\n          src={beforeSrc}\n          alt={beforeAlt}\n          className=\"pointer-events-none absolute inset-0 size-full object-cover select-none\"\n          draggable={false}\n        />\n        {showLabels && (\n          <span className=\"bg-background/80 text-foreground absolute bottom-2 left-2 rounded px-2 py-0.5 text-xs font-medium backdrop-blur-sm\">\n            {beforeLabel}\n          </span>\n        )}\n\n        {/* After (clipped overlay — visible on the right side) */}\n        <div className=\"absolute inset-0 size-full\" style={clipStyle}>\n          <img\n            src={afterSrc}\n            alt={afterAlt}\n            className=\"pointer-events-none absolute inset-0 size-full object-cover select-none\"\n            draggable={false}\n          />\n          {showLabels && (\n            <span className=\"bg-background/80 text-foreground absolute right-2 bottom-2 rounded px-2 py-0.5 text-xs font-medium backdrop-blur-sm\">\n              {afterLabel}\n            </span>\n          )}\n        </div>\n\n        {/* Divider + handle */}\n        {!disabled && (\n          <div\n            className={cn(\n              'bg-border absolute z-10',\n              orientation === 'horizontal' ? 'top-0 h-full w-0.5' : 'left-0 h-0.5 w-full',\n            )}\n            style={dividerStyle}\n          >\n            {showHandle && (\n              <button\n                type=\"button\"\n                role=\"slider\"\n                aria-valuenow={Math.round(position)}\n                aria-valuemin={0}\n                aria-valuemax={100}\n                aria-label={`Image comparison slider, ${Math.round(position)} percent`}\n                aria-orientation={orientation}\n                tabIndex={0}\n                className={cn(\n                  'bg-background border-border focus-visible:ring-ring absolute flex size-9 cursor-ew-resize items-center justify-center rounded-full border shadow-md transition-transform hover:scale-110 focus-visible:ring-2 focus-visible:outline-none',\n                  'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',\n                  orientation === 'vertical' && 'cursor-ns-resize',\n                )}\n                onKeyDown={onKeyDown}\n                onPointerDown={(e) => {\n                  e.stopPropagation()\n                  onPointerDown(e)\n                }}\n                onPointerMove={onPointerMove}\n                onPointerUp={onPointerUp}\n                onPointerCancel={onPointerUp}\n              >\n                {handle !== undefined ? (\n                  handle\n                ) : orientation === 'horizontal' ? (\n                  <MoveHorizontal className=\"text-foreground size-4\" />\n                ) : (\n                  <MoveVertical className=\"text-foreground size-4\" />\n                )}\n              </button>\n            )}\n          </div>\n        )}\n\n        {disabled && <div className=\"bg-background/40 absolute inset-0\" />}\n      </div>\n    )\n  },\n)\nImageCompare.displayName = 'ImageCompare'\n\nexport { ImageCompare }\n",
      "type": "registry:ui",
      "target": "~/components/ui/image-compare/ImageCompare.tsx"
    },
    {
      "path": "packages/registry-react/components/image-compare/image-compare.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\nexport const imageCompareVariants = cva('bg-muted relative overflow-hidden rounded-lg border select-none', {\n  variants: {\n    orientation: {\n      horizontal: 'cursor-ew-resize',\n      vertical: 'cursor-ns-resize',\n    },\n  },\n  defaultVariants: {\n    orientation: 'horizontal',\n  },\n})\n\nexport type ImageCompareVariants = VariantProps<typeof imageCompareVariants>\n",
      "type": "registry:ui",
      "target": "~/components/ui/image-compare/image-compare.variants.ts"
    },
    {
      "path": "packages/registry-react/components/image-compare/index.ts",
      "content": "export { ImageCompare, type ImageCompareProps } from './ImageCompare'\nexport { imageCompareVariants, type ImageCompareVariants } from './image-compare.variants'\n",
      "type": "registry:ui",
      "target": "~/components/ui/image-compare/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Before/after image comparison slider with a draggable divider. Two images overlaid via clip-path, pointer-driven (mouse + touch), horizontal or vertical orientation, custom handle render prop, labels, and disabled state.",
  "categories": [
    "display",
    "media"
  ]
}