{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-comparison",
  "type": "registry:component",
  "title": "Image Comparison — Stated vs Revealed as a gesture",
  "description": "Motion Primitives' before/after clip-path slider (MIT, first MP adoption), bridged to the contract. Compound API: ImageComparison + Image/Content panes + Slider. The brand's canonical use compares a stated claim against revealed behavior in one drag.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://design.subconscious.ai/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/components/image-comparison.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @ibelick (Motion Primitives)\n * @description: Image Comparison — before/after clip-path slider\n * @license: MIT\n * @website: https://motion-primitives.com/docs/image-comparison\n * @source: https://github.com/ibelick/motion-primitives/blob/main/components/core/image-comparison.tsx\n * @upstream-revision: 92586e62a951 (2026-03-19)\n * @adopted: 2026-08-07\n *\n * The first Motion Primitives adoption, and the most on-brand component in\n * either source library (DECISIONS.md §\"The curated map\"): a before/after\n * drag slider IS Stated-vs-Revealed as an interaction — control vs\n * treatment, one gesture.\n *\n * Bridged per the doctrine filter (re-map the paint, keep the character):\n *\n *  - Kept verbatim: the spring-driven MotionValue, the clip-path split, the\n *    compound Provider/Image/Slider API, hover and drag modes, touch\n *    handling. This is the engineering we adopted it for.\n *  - Runtime: `motion/react` — the registry's enforced runtime (T2 landed\n *    2026-08-12; framer-motion is retired and the registry tests enforce\n *    that it never reappears).\n *  - Added in upstream's exact idiom: `ImageComparisonContent`, a DOM-pane\n *    twin of ImageComparisonImage. The brand's canonical use compares\n *    *content* (a stated claim vs revealed behavior), not two photographs;\n *    same clipPath transform, same position prop, a <motion.div> instead\n *    of <motion.img>.\n *  - No paint shipped: the component carries no colors of its own beyond\n *    upstream's neutral slider bar; panes and slider are styled by the\n *    caller with contract tokens.\n */\n\nimport { cn } from \"@/registry/lib/utils\";\nimport { useState, createContext, useContext } from \"react\";\nimport {\n  motion,\n  MotionValue,\n  SpringOptions,\n  useMotionValue,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\n\nconst ImageComparisonContext = createContext<\n  | {\n      sliderPosition: number;\n      setSliderPosition: (pos: number) => void;\n      motionSliderPosition: MotionValue<number>;\n    }\n  | undefined\n>(undefined);\n\nexport type ImageComparisonProps = {\n  children: React.ReactNode;\n  className?: string;\n  enableHover?: boolean;\n  springOptions?: SpringOptions;\n};\n\nconst DEFAULT_SPRING_OPTIONS = {\n  bounce: 0,\n  duration: 0,\n};\n\nfunction ImageComparison({\n  children,\n  className,\n  enableHover,\n  springOptions,\n}: ImageComparisonProps) {\n  const [isDragging, setIsDragging] = useState(false);\n  const motionValue = useMotionValue(50);\n  const motionSliderPosition = useSpring(\n    motionValue,\n    springOptions ?? DEFAULT_SPRING_OPTIONS\n  );\n  const [sliderPosition, setSliderPosition] = useState(50);\n\n  const handleDrag = (event: React.PointerEvent) => {\n    if (!isDragging && !enableHover) return;\n\n    const containerRect = (\n      event.currentTarget as HTMLElement\n    ).getBoundingClientRect();\n    const x = event.clientX - containerRect.left;\n\n    const percentage = Math.min(\n      Math.max((x / containerRect.width) * 100, 0),\n      100\n    );\n    motionValue.set(percentage);\n    setSliderPosition(percentage);\n  };\n\n  /* Pointer capture keeps the drag alive when the pointer leaves the\n   * rectangle mid-drag — without it the slider dies at the border and\n   * snaps back, which reads as broken (owner report, 2026-08-12). The\n   * position is clamped, so off-element moves are still correct. Pointer\n   * events cover mouse + touch + pen in one path. */\n  return (\n    <ImageComparisonContext.Provider\n      value={{ sliderPosition, setSliderPosition, motionSliderPosition }}\n    >\n      <div\n        className={cn(\n          \"relative select-none overflow-hidden touch-none\",\n          enableHover && \"cursor-ew-resize\",\n          className\n        )}\n        onPointerMove={handleDrag}\n        onPointerDown={(event) => {\n          if (enableHover) return;\n          event.currentTarget.setPointerCapture?.(event.pointerId);\n          setIsDragging(true);\n        }}\n        onPointerUp={(event) => {\n          if (enableHover) return;\n          if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {\n            event.currentTarget.releasePointerCapture(event.pointerId);\n          }\n          setIsDragging(false);\n        }}\n        onPointerCancel={() => !enableHover && setIsDragging(false)}\n      >\n        {children}\n      </div>\n    </ImageComparisonContext.Provider>\n  );\n}\n\nconst ImageComparisonImage = ({\n  className,\n  alt,\n  src,\n  position,\n}: {\n  className?: string;\n  alt: string;\n  src: string;\n  position: \"left\" | \"right\";\n}) => {\n  const { motionSliderPosition } = useContext(ImageComparisonContext)!;\n  const leftClipPath = useTransform(\n    motionSliderPosition,\n    (value) => `inset(0 0 0 ${value}%)`\n  );\n  const rightClipPath = useTransform(\n    motionSliderPosition,\n    (value) => `inset(0 ${100 - value}% 0 0)`\n  );\n\n  return (\n    <motion.img\n      src={src}\n      alt={alt}\n      className={cn(\"absolute inset-0 h-full w-full object-cover\", className)}\n      style={{\n        clipPath: position === \"left\" ? leftClipPath : rightClipPath,\n      }}\n    />\n  );\n};\n\n/* DOM-pane twin of ImageComparisonImage — same clipPath transform, same\n   position prop, a div instead of an img. The canonical brand use compares\n   content (a stated claim vs the revealed behavior), not two photographs. */\nconst ImageComparisonContent = ({\n  className,\n  children,\n  position,\n}: {\n  className?: string;\n  children: React.ReactNode;\n  position: \"left\" | \"right\";\n}) => {\n  const { motionSliderPosition } = useContext(ImageComparisonContext)!;\n  const leftClipPath = useTransform(\n    motionSliderPosition,\n    (value) => `inset(0 0 0 ${value}%)`\n  );\n  const rightClipPath = useTransform(\n    motionSliderPosition,\n    (value) => `inset(0 ${100 - value}% 0 0)`\n  );\n\n  return (\n    <motion.div\n      className={cn(\"absolute inset-0 h-full w-full\", className)}\n      style={{\n        clipPath: position === \"left\" ? leftClipPath : rightClipPath,\n      }}\n    >\n      {children}\n    </motion.div>\n  );\n};\n\nconst ImageComparisonSlider = ({\n  className,\n  children,\n}: {\n  className: string;\n  children?: React.ReactNode;\n}) => {\n  const { motionSliderPosition } = useContext(ImageComparisonContext)!;\n\n  const left = useTransform(motionSliderPosition, (value) => `${value}%`);\n\n  return (\n    <motion.div\n      className={cn(\"absolute bottom-0 top-0 w-1 cursor-ew-resize\", className)}\n      style={{\n        left,\n      }}\n    >\n      {children}\n    </motion.div>\n  );\n};\n\nexport {\n  ImageComparison,\n  ImageComparisonImage,\n  ImageComparisonContent,\n  ImageComparisonSlider,\n};\n"
    }
  ]
}
