{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "image-compare",
  "title": "Image Compare",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/image-compare/ImageCompare.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n  import type { ImageCompareVariants } from './image-compare.variants'\n\n  export interface ImageCompareProps extends HTMLAttributes<HTMLDivElement> {\n    beforeSrc: string\n    afterSrc: string\n    beforeAlt?: string\n    afterAlt?: string\n    beforeLabel?: string\n    afterLabel?: string\n    /** Two-way bound divider position in percent (`bind:value`). */\n    value?: number\n    orientation?: ImageCompareVariants['orientation']\n    disabled?: boolean\n    showLabels?: boolean\n    showHandle?: boolean\n    /** Custom handle content (replaces the move icon). */\n    handle?: Snippet\n    /** Fires with the new position whenever the user drags or arrows the divider. */\n    onValueChange?: (value: number) => void\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { MoveHorizontal, MoveVertical } from '@lucide/svelte'\n  import { imageCompareVariants } from './image-compare.variants'\n\n  let {\n    class: className,\n    beforeSrc,\n    afterSrc,\n    beforeAlt = 'Before',\n    afterAlt = 'After',\n    beforeLabel = 'Before',\n    afterLabel = 'After',\n    value = $bindable(50),\n    orientation = 'horizontal',\n    disabled = false,\n    showLabels = true,\n    showHandle = true,\n    handle,\n    onValueChange,\n    ref = $bindable(null),\n    ...restProps\n  }: ImageCompareProps = $props()\n\n  let dragging = $state(false)\n\n  function clamp(v: number): number {\n    return Math.min(100, Math.max(0, v))\n  }\n\n  function updateFromPointer(clientX: number, clientY: number) {\n    const el = ref\n    if (!el) return\n    const rect = el.getBoundingClientRect()\n    const next =\n      orientation === 'horizontal'\n        ? clamp(((clientX - rect.left) / rect.width) * 100)\n        : clamp(((clientY - rect.top) / rect.height) * 100)\n    value = next\n    onValueChange?.(next)\n  }\n\n  function onPointerDown(e: PointerEvent & { currentTarget: HTMLElement }) {\n    if (disabled) return\n    dragging = true\n    try {\n      e.currentTarget.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: PointerEvent) {\n    if (!dragging || disabled) return\n    updateFromPointer(e.clientX, e.clientY)\n  }\n\n  function onPointerUp(e: PointerEvent & { currentTarget: HTMLElement }) {\n    if (!dragging) return\n    dragging = false\n    try {\n      e.currentTarget.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: KeyboardEvent) {\n    if (disabled) return\n    const isH = orientation === 'horizontal'\n    const step = e.shiftKey ? 10 : 1\n    let next = 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    value = clamp(next)\n    onValueChange?.(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.\n  const clipStyle = $derived(\n    orientation === 'horizontal' ? `clip-path: inset(0 0 0 ${value}%)` : `clip-path: inset(${value}% 0 0 0)`,\n  )\n  const dividerStyle = $derived(orientation === 'horizontal' ? `left: ${value}%` : `top: ${value}%`)\n</script>\n\n<div\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"image-compare\"\n  data-orientation={orientation}\n  data-disabled={disabled ? '' : undefined}\n  class={cn(imageCompareVariants({ orientation }), className)}\n  style=\"touch-action: none\"\n  onpointerdown={onPointerDown}\n  onpointermove={onPointerMove}\n  onpointerup={onPointerUp}\n  onpointercancel={onPointerUp}\n  {...restProps}\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  {#if showLabels}\n    <span\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  {/if}\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    {#if showLabels}\n      <span\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    {/if}\n  </div>\n\n  <!-- Divider + handle -->\n  {#if !disabled}\n    <div\n      class=\"bg-border absolute z-10 {orientation === 'horizontal' ? 'top-0 h-full w-0.5' : 'left-0 h-0.5 w-full'}\"\n      style={dividerStyle}\n    >\n      {#if showHandle}\n        <button\n          type=\"button\"\n          role=\"slider\"\n          aria-valuenow={Math.round(value)}\n          aria-valuemin=\"0\"\n          aria-valuemax=\"100\"\n          aria-label={`Image comparison slider, ${Math.round(value)} percent`}\n          aria-orientation={orientation}\n          tabindex=\"0\"\n          class=\"bg-background border-border focus-visible:ring-ring absolute top-1/2 left-1/2 flex size-9 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border shadow-md transition-transform hover:scale-110 focus-visible:ring-2 focus-visible:outline-none {orientation ===\n          'vertical'\n            ? 'cursor-ns-resize'\n            : 'cursor-ew-resize'}\"\n          onkeydown={onKeyDown}\n          onpointerdown={(e) => {\n            e.stopPropagation()\n            onPointerDown(e)\n          }}\n          onpointermove={onPointerMove}\n          onpointerup={onPointerUp}\n          onpointercancel={onPointerUp}\n        >\n          {#if handle}\n            {@render handle()}\n          {:else if orientation === 'horizontal'}\n            <MoveHorizontal class=\"text-foreground size-4\" />\n          {:else}\n            <MoveVertical class=\"text-foreground size-4\" />\n          {/if}\n        </button>\n      {/if}\n    </div>\n  {/if}\n\n  {#if disabled}\n    <div class=\"bg-background/40 absolute inset-0\"></div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/image-compare/ImageCompare.svelte"
    },
    {
      "path": "packages/registry-svelte/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": "~/src/lib/components/ui/image-compare/image-compare.variants.ts"
    },
    {
      "path": "packages/registry-svelte/components/image-compare/index.ts",
      "content": "export { default as ImageCompare, type ImageCompareProps } from './ImageCompare.svelte'\nexport { imageCompareVariants, type ImageCompareVariants } from './image-compare.variants'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/image-compare/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "@lucide/svelte"
  ],
  "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 snippet, labels, and disabled state.",
  "categories": [
    "display",
    "media"
  ]
}