{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "image-compare",
  "title": "Image Compare",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/image-compare/ImageCompare.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { MoveHorizontal, MoveVertical } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { imageCompareVariants } from './image-compare.variants'\n\ninterface Props {\n  beforeSrc: string\n  afterSrc: string\n  beforeAlt?: string\n  afterAlt?: string\n  beforeLabel?: string\n  afterLabel?: string\n  modelValue?: number\n  orientation?: 'horizontal' | 'vertical'\n  disabled?: boolean\n  showLabels?: boolean\n  showHandle?: boolean\n  class?: HTMLAttributes['class']\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  beforeAlt: 'Before',\n  afterAlt: 'After',\n  beforeLabel: 'Before',\n  afterLabel: 'After',\n  modelValue: 50,\n  orientation: 'horizontal',\n  disabled: false,\n  showLabels: true,\n  showHandle: true,\n})\n\nconst emit = defineEmits<{\n  'update:modelValue': [value: number]\n}>()\n\nconst containerRef = ref<HTMLElement | null>(null)\nconst handleRef = ref<HTMLElement | null>(null)\nconst dragging = ref(false)\nconst position = ref(props.modelValue)\n\nwatch(\n  () => props.modelValue,\n  (v) => {\n    if (v !== position.value) position.value = v\n  },\n)\n\nfunction clamp(v: number): number {\n  return Math.min(100, Math.max(0, v))\n}\n\nfunction updateFromPointer(clientX: number, clientY: number) {\n  const el = containerRef.value\n  if (!el) return\n  const rect = el.getBoundingClientRect()\n  if (props.orientation === 'horizontal') {\n    position.value = clamp(((clientX - rect.left) / rect.width) * 100)\n  } else {\n    position.value = clamp(((clientY - rect.top) / rect.height) * 100)\n  }\n  emit('update:modelValue', position.value)\n}\n\nfunction onPointerDown(e: PointerEvent) {\n  if (props.disabled) return\n  dragging.value = 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\nfunction onPointerMove(e: PointerEvent) {\n  if (!dragging.value || props.disabled) return\n  updateFromPointer(e.clientX, e.clientY)\n}\n\nfunction onPointerUp(e: PointerEvent) {\n  if (!dragging.value) return\n  dragging.value = 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%)\nfunction onKeyDown(e: KeyboardEvent) {\n  if (props.disabled) return\n  const isH = props.orientation === 'horizontal'\n  const step = e.shiftKey ? 10 : 1\n  let next = position.value\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  position.value = clamp(next)\n  emit('update:modelValue', position.value)\n}\n\n// \"After\" image is clipped to show only the right portion.\n// Dragging right (higher %) reveals more of \"before\" on the left.\nconst clipStyle = computed(() => {\n  const pct = position.value\n  if (props.orientation === 'horizontal') {\n    return { clipPath: `inset(0 0 0 ${pct}%)` }\n  }\n  return { clipPath: `inset(${pct}% 0 0 0)` }\n})\n\nconst dividerStyle = computed(() => {\n  const pct = position.value\n  if (props.orientation === 'horizontal') {\n    return { left: `${pct}%` }\n  }\n  return { top: `${pct}%` }\n})\n\nonBeforeUnmount(() => {\n  dragging.value = false\n})\n</script>\n\n<template>\n  <div\n    ref=\"containerRef\"\n    data-uipkge\n    data-slot=\"image-compare\"\n    :data-orientation=\"orientation\"\n    :data-disabled=\"disabled ? '' : undefined\"\n    :class=\"cn(imageCompareVariants({ orientation }), props.class)\"\n    style=\"touch-action: none\"\n    @pointerdown=\"onPointerDown\"\n    @pointermove=\"onPointerMove\"\n    @pointerup=\"onPointerUp\"\n    @pointercancel=\"onPointerUp\"\n  >\n    <!-- Before (base layer, full) -->\n    <img\n      :src=\"beforeSrc\"\n      :alt=\"beforeAlt\"\n      class=\"pointer-events-none absolute inset-0 size-full object-cover select-none\"\n      draggable=\"false\"\n    />\n    <span\n      v-if=\"showLabels\"\n      class=\"bg-background/80 text-foreground absolute bottom-2 left-2 rounded px-2 py-0.5 text-xs font-medium backdrop-blur-sm\"\n    >\n      {{ beforeLabel }}\n    </span>\n\n    <!-- After (clipped overlay — visible on the right side) -->\n    <div class=\"absolute inset-0 size-full\" :style=\"clipStyle\">\n      <img\n        :src=\"afterSrc\"\n        :alt=\"afterAlt\"\n        class=\"pointer-events-none absolute inset-0 size-full object-cover select-none\"\n        draggable=\"false\"\n      />\n      <span\n        v-if=\"showLabels\"\n        class=\"bg-background/80 text-foreground absolute right-2 bottom-2 rounded px-2 py-0.5 text-xs font-medium backdrop-blur-sm\"\n      >\n        {{ afterLabel }}\n      </span>\n    </div>\n\n    <!-- Divider + handle -->\n    <div\n      v-if=\"!disabled\"\n      class=\"bg-border absolute z-10\"\n      :class=\"orientation === 'horizontal' ? 'top-0 h-full w-0.5' : 'left-0 h-0.5 w-full'\"\n      :style=\"dividerStyle\"\n    >\n      <button\n        v-if=\"showHandle\"\n        ref=\"handleRef\"\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        class=\"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        :class=\"orientation === 'horizontal' ? 'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2' : 'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'\"\n        :style=\"orientation === 'vertical' ? { cursor: 'ns-resize' } : {}\"\n        @keydown=\"onKeyDown\"\n        @pointerdown.stop=\"onPointerDown\"\n        @pointermove=\"onPointerMove\"\n        @pointerup=\"onPointerUp\"\n        @pointercancel=\"onPointerUp\"\n      >\n        <slot name=\"handle\">\n          <MoveHorizontal v-if=\"orientation === 'horizontal'\" class=\"text-foreground size-4\" />\n          <MoveVertical v-else class=\"text-foreground size-4\" />\n        </slot>\n      </button>\n    </div>\n\n    <div v-if=\"disabled\" class=\"bg-background/40 absolute inset-0\" />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/image-compare/ImageCompare.vue"
    },
    {
      "path": "packages/registry-vue/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": "~/app/components/ui/image-compare/image-compare.variants.ts"
    },
    {
      "path": "packages/registry-vue/components/image-compare/index.ts",
      "content": "export { default as ImageCompare } from './ImageCompare.vue'\nexport { imageCompareVariants, type ImageCompareVariants } from './image-compare.variants'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/image-compare/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lucide-vue-next"
  ],
  "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 slot, labels, and disabled state.",
  "categories": [
    "display",
    "media"
  ]
}