{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "image-compare",
  "title": "Image Compare",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/image-compare/image-compare.component.ts",
      "content": "import {\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  Output,\n  TemplateRef,\n  booleanAttribute,\n  inject,\n  numberAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { UiRenderTemplateDirective } from '@/ui/popper/popper'\nimport { imageCompareVariants, type ImageCompareVariants } from './image-compare.variants'\n\nexport type ImageCompareOrientation = NonNullable<ImageCompareVariants['orientation']>\n\nfunction clamp(v: number): number {\n  return Math.min(100, Math.max(0, v))\n}\n\n/**\n * Angular port of the React ImageCompare: the \"before\" image fills the frame, the \"after\"\n * image sits on top clipped with `clip-path: inset(...)`, and a divider line carries a\n * focusable `<button role=\"slider\">` handle. Pointer drag anywhere on the frame (pointer\n * capture), Arrow keys on the handle move 1% (Shift = 10%), horizontal or vertical.\n * `value` / `defaultValue` / `valueChange` (`[(value)]`, 0–100); `handle` is a template\n * that replaces the default arrow icon.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-image-compare, [ui-image-compare]',\n  standalone: true,\n  imports: [UiRenderTemplateDirective],\n  host: {\n    'data-uipkge': '',\n    'data-slot': 'image-compare',\n    '[attr.data-orientation]': 'orientation',\n    '[attr.data-disabled]': 'disabled ? \"\" : null',\n    '[class]': 'hostClass',\n    '(pointerdown)': 'onPointerDown($event)',\n    '(pointermove)': 'onPointerMove($event)',\n    '(pointerup)': 'onPointerUp($event)',\n    '(pointercancel)': 'onPointerUp($event)',\n  },\n  template: `\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        >{{ beforeLabel }}</span\n      >\n    }\n    <div class=\"absolute inset-0 size-full\" [style.clip-path]=\"clipPath\">\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          >{{ afterLabel }}</span\n        >\n      }\n    </div>\n    @if (!disabled) {\n      <div\n        [class]=\"dividerClass\"\n        [style.left]=\"orientation === 'horizontal' ? position + '%' : null\"\n        [style.top]=\"orientation === 'vertical' ? position + '%' : null\"\n      >\n        @if (showHandle) {\n          <button\n            type=\"button\"\n            role=\"slider\"\n            [attr.aria-valuenow]=\"rounded\"\n            aria-valuemin=\"0\"\n            aria-valuemax=\"100\"\n            [attr.aria-label]=\"'Image comparison slider, ' + rounded + ' percent'\"\n            [attr.aria-orientation]=\"orientation === 'vertical' ? 'vertical' : 'horizontal'\"\n            tabindex=\"0\"\n            [class]=\"handleClass\"\n            (keydown)=\"onKeyDown($event)\"\n            (pointerdown)=\"$event.stopPropagation(); onPointerDown($event)\"\n            (pointermove)=\"$event.stopPropagation(); onPointerMove($event)\"\n            (pointerup)=\"$event.stopPropagation(); onPointerUp($event)\"\n            (pointercancel)=\"$event.stopPropagation(); onPointerUp($event)\"\n          >\n            @if (handle) {\n              <ng-container [uiRenderTemplate]=\"handle\" />\n            } @else if (orientation === 'horizontal') {\n              <svg\n                xmlns=\"http://www.w3.org/2000/svg\"\n                width=\"24\"\n                height=\"24\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                stroke-width=\"2\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n                class=\"lucide lucide-move-horizontal text-foreground size-4\"\n                aria-hidden=\"true\"\n              >\n                <path d=\"m18 8 4 4-4 4\" />\n                <path d=\"M2 12h20\" />\n                <path d=\"m6 8-4 4 4 4\" />\n              </svg>\n            } @else {\n              <svg\n                xmlns=\"http://www.w3.org/2000/svg\"\n                width=\"24\"\n                height=\"24\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                stroke-width=\"2\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n                class=\"lucide lucide-move-vertical text-foreground size-4\"\n                aria-hidden=\"true\"\n              >\n                <path d=\"M12 2v20\" />\n                <path d=\"m8 18 4 4 4-4\" />\n                <path d=\"m8 6 4-4 4 4\" />\n              </svg>\n            }\n          </button>\n        }\n      </div>\n    }\n    @if (disabled) {\n      <div class=\"bg-background/40 absolute inset-0\"></div>\n    }\n  `,\n})\nexport class UiImageCompareComponent {\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef)\n  private readonly _value = signal<number | undefined>(undefined)\n  private readonly _internal = signal<number | null>(null)\n  private dragging = false\n\n  @Input({ required: true }) beforeSrc!: string\n  @Input({ required: true }) afterSrc!: string\n  @Input() beforeAlt = 'Before'\n  @Input() afterAlt = 'After'\n  @Input() beforeLabel = 'Before'\n  @Input() afterLabel = 'After'\n  /** Controlled slider position (0–100). */\n  @Input()\n  set value(v: number | null | undefined) {\n    this._value.set(v ?? undefined)\n  }\n  get value(): number {\n    return this._value() ?? this._internal() ?? this.defaultValue\n  }\n  /** Uncontrolled initial position (0–100). */\n  @Input({ transform: numberAttribute }) defaultValue = 50\n  /** Fired with the new position (0–100) on drag / keyboard move. */\n  @Output() valueChange = new EventEmitter<number>()\n  @Input() orientation: ImageCompareOrientation = 'horizontal'\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) showLabels = true\n  @Input({ transform: booleanAttribute }) showHandle = true\n  /** Custom handle content — replaces the default arrow icon. */\n  @Input() handle?: TemplateRef<unknown>\n  @Input('class') className?: string\n\n  get position(): number {\n    return this.value\n  }\n\n  get rounded(): number {\n    return Math.round(this.position)\n  }\n\n  get hostClass(): string {\n    // React's root is a <div> with touch-action: none; `block` + `touch-none` give the same box.\n    return cn('block touch-none', imageCompareVariants({ orientation: this.orientation }), this.className)\n  }\n\n  get clipPath(): string {\n    const pct = this.position\n    return this.orientation === 'horizontal' ? `inset(0 0 0 ${pct}%)` : `inset(${pct}% 0 0 0)`\n  }\n\n  get dividerClass(): string {\n    return cn(\n      'bg-border absolute z-10',\n      this.orientation === 'horizontal' ? 'top-0 h-full w-0.5' : 'left-0 h-0.5 w-full',\n    )\n  }\n\n  get handleClass(): string {\n    return 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      'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',\n      this.orientation === 'vertical' && 'cursor-ns-resize',\n    )\n  }\n\n  private setPosition(next: number): void {\n    const clamped = clamp(next)\n    if (this._value() === undefined) this._internal.set(clamped)\n    this.valueChange.emit(clamped)\n  }\n\n  private updateFromPointer(clientX: number, clientY: number): void {\n    const rect = this.el.nativeElement.getBoundingClientRect()\n    if (this.orientation === 'horizontal') this.setPosition(((clientX - rect.left) / rect.width) * 100)\n    else this.setPosition(((clientY - rect.top) / rect.height) * 100)\n  }\n\n  onPointerDown(e: PointerEvent): void {\n    if (this.disabled) return\n    this.dragging = true\n    try {\n      ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)\n    } catch {\n      // pointer capture can fail on synthetic / non-active pointers\n    }\n    this.updateFromPointer(e.clientX, e.clientY)\n  }\n\n  onPointerMove(e: PointerEvent): void {\n    if (!this.dragging || this.disabled) return\n    this.updateFromPointer(e.clientX, e.clientY)\n  }\n\n  onPointerUp(e: PointerEvent): void {\n    if (!this.dragging) return\n    this.dragging = false\n    try {\n      ;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId)\n    } catch {\n      // pointer already released\n    }\n  }\n\n  /** Arrow keys move the slider by 1% (Shift = 10%). */\n  onKeyDown(e: KeyboardEvent): void {\n    if (this.disabled) return\n    const step = e.shiftKey ? 10 : 1\n    let next = this.position\n    if (this.orientation === 'horizontal') {\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    this.setPosition(next)\n  }\n}\n\nexport { imageCompareVariants, type ImageCompareVariants }\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/image-compare/image-compare.component.ts"
    },
    {
      "path": "packages/registry-angular/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/app/components/ui/image-compare/image-compare.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/image-compare/index.ts",
      "content": "export {\n  UiImageCompareComponent,\n  imageCompareVariants,\n  type ImageCompareVariants,\n  type ImageCompareOrientation,\n} from './image-compare.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/image-compare/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "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 template, labels, and disabled state.",
  "categories": [
    "display",
    "media"
  ]
}