{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "before-after",
  "title": "Before After",
  "description": "Two versions of one thing, compared with a divider you drag or a lens that follows the pointer. A real slider, keyboard included.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/before-after.tsx",
      "content": "import type * as React from \"react\"\nimport { useRef } from \"react\"\nimport { motion, useMotionTemplate } from \"motion/react\"\nimport { ChevronsLeftRight, ChevronsUpDown } from \"lucide-react\"\nimport { MAX_SHARE, MIN_SHARE, shareForKey, type BeforeAfterVariant } from \"@/registry/miseui/ui/before-after-geometry\"\nimport { useComparison, useFrameDrag, useSweepOnce, type Comparison } from \"@/registry/miseui/ui/before-after-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype BeforeAfterEffect = \"sweep\"\n\nconst CURSOR: Record<BeforeAfterVariant, string> = { split: \"cursor-ew-resize\", curtain: \"cursor-ns-resize\", lens: \"cursor-crosshair\" }\n// The first axis is the one the divider travels on, so the page can still scroll along the other one.\nconst TOUCH: Record<BeforeAfterVariant, string> = { split: \"touch-pan-y\", curtain: \"touch-pan-x\", lens: \"touch-pan-y\" }\n\nconst RAIL: Record<BeforeAfterVariant, string> = {\n  split: \"inset-y-0 w-px bg-background shadow-[0_0_0_1px_rgb(0_0_0/0.25)]\",\n  curtain: \"inset-x-0 h-px bg-background shadow-[0_0_0_1px_rgb(0_0_0/0.25)]\",\n  lens: \"size-0\",\n}\n\ntype RootAttributes = Omit<React.ComponentProps<\"figure\">, \"children\" | \"onChange\">\n\ninterface BeforeAfterProps extends RootAttributes {\n  /** The first version: an image with `size-full object-cover`, or any markup. It cannot be interacted with here. */\n  before: React.ReactNode\n  after: React.ReactNode\n  beforeLabel?: string\n  afterLabel?: string\n  /** Name of the slider for assistive tech. */\n  label?: string\n  /** `split` a vertical divider, `curtain` a horizontal one, `lens` a round window that follows the pointer. */\n  variant?: BeforeAfterVariant\n  /** `sweep` moves the divider once when it first comes into view, to show it can be moved. */\n  effect?: BeforeAfterEffect\n  value?: number\n  defaultValue?: number\n  onValueChange?: (share: number) => void\n  step?: number\n  disabled?: boolean\n  caption?: React.ReactNode\n}\n\nfunction Chip({ className, children }: { className: string; children: React.ReactNode }) {\n  return <span className={cn(\"absolute rounded-md bg-background/85 px-2 py-1 text-[13px] leading-none\", className)}>{children}</span>\n}\n\ninterface HandleProps {\n  variant: BeforeAfterVariant\n  comparison: Comparison\n  label: string\n  initial: number\n  step: number\n  disabled: boolean\n}\n\nfunction DividerHandle({ variant, comparison, label, initial, step, disabled }: HandleProps) {\n  const { handleRef, share, cross, report, glideTo } = comparison\n  const percent = useMotionTemplate`${share}%`\n  const crossPercent = useMotionTemplate`${cross}%`\n  const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    const next = shareForKey(event.key, share.get(), step)\n    if (next === null) return\n    event.preventDefault()\n    glideTo(next)\n    report(next)\n  }\n  const Icon = variant === \"curtain\" ? ChevronsUpDown : ChevronsLeftRight\n  const place = {\n    split: { left: percent },\n    curtain: { top: percent },\n    lens: { left: percent, top: crossPercent },\n  }[variant]\n  return (\n    <motion.div data-slot=\"before-after-rail\" className={cn(\"absolute\", RAIL[variant])} style={place}>\n      <div\n        ref={handleRef}\n        role=\"slider\"\n        tabIndex={disabled ? -1 : 0}\n        aria-label={label}\n        aria-valuemin={MIN_SHARE}\n        aria-valuemax={MAX_SHARE}\n        aria-valuenow={Math.round(initial)}\n        aria-orientation={variant === \"curtain\" ? \"vertical\" : \"horizontal\"}\n        aria-disabled={disabled || undefined}\n        data-slot=\"before-after-handle\"\n        onKeyDown={disabled ? undefined : onKeyDown}\n        className={cn(\n          \"absolute top-1/2 left-1/2 grid -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full outline-none\",\n          \"focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n          variant === \"lens\"\n            ? \"size-[min(144px,56cqw)] shadow-[0_0_0_2px_var(--background),0_0_0_3px_rgb(0_0_0/0.25),0_8px_24px_-8px_rgb(0_0_0/0.5)]\"\n            : \"size-10 border bg-background text-foreground shadow-[0_4px_12px_-4px_rgb(0_0_0/0.5)]\",\n        )}\n      >\n        {variant === \"lens\" ? null : <Icon aria-hidden=\"true\" className=\"size-4\" />}\n      </div>\n    </motion.div>\n  )\n}\n\nfunction BeforeAfter({\n  before, after, beforeLabel = \"Before\", afterLabel = \"After\", label = \"Compare before and after\",\n  variant = \"split\", effect, value, defaultValue = 50, onValueChange, step = 5, disabled = false, caption, className, ...props\n}: BeforeAfterProps) {\n  const frameRef = useRef<HTMLDivElement>(null)\n  const comparison = useComparison(variant, value, defaultValue, onValueChange)\n  const handlers = useFrameDrag(variant, comparison)\n  useSweepOnce(frameRef, comparison, effect === \"sweep\" && !comparison.reduceMotion && !disabled)\n  const lens = variant === \"lens\"\n  return (\n    <figure\n      data-slot=\"before-after\"\n      data-variant={variant}\n      data-effect={effect}\n      className={cn(\"grid w-full min-w-0 grid-cols-[minmax(0,1fr)] gap-2\", className)}\n      {...props}\n    >\n      <div\n        ref={frameRef}\n        data-slot=\"before-after-frame\"\n        className={cn(\"@container relative aspect-[16/10] overflow-hidden rounded-xl border bg-muted select-none\", !disabled && [CURSOR[variant], TOUCH[variant]], disabled && \"opacity-50\")}\n        {...(disabled ? undefined : handlers)}\n      >\n        <div className=\"pointer-events-none absolute inset-0\">\n          {lens ? before : after}\n          <Chip className={lens ? \"top-3 left-3\" : \"top-3 right-3\"}>{lens ? beforeLabel : afterLabel}</Chip>\n        </div>\n        <motion.div data-slot=\"before-after-clip\" className=\"pointer-events-none absolute inset-0\" style={{ clipPath: comparison.clip }}>\n          {lens ? after : before}\n          {/* The lens would cut a chip in half with its round edge, and what it shows is obvious once it moves. */}\n          {lens ? null : <Chip className=\"top-3 left-3\">{beforeLabel}</Chip>}\n        </motion.div>\n        <DividerHandle variant={variant} comparison={comparison} label={label} initial={value ?? defaultValue} step={step} disabled={disabled} />\n      </div>\n      {caption ? <figcaption className=\"text-[13px] text-muted-foreground\">{caption}</figcaption> : null}\n    </figure>\n  )\n}\n\nexport { BeforeAfter, type BeforeAfterProps, type BeforeAfterVariant, type BeforeAfterEffect }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/before-after-geometry.ts",
      "content": "export type BeforeAfterVariant = \"split\" | \"curtain\" | \"lens\"\n\nexport const MIN_SHARE = 0\nexport const MAX_SHARE = 100\nconst PAGE_STEP_MULTIPLE = 2\nconst LENS_RADIUS = \"min(72px, 28cqw)\"\n\nexport function clampShare(share: number) {\n  if (Number.isNaN(share)) return MIN_SHARE\n  return Math.min(MAX_SHARE, Math.max(MIN_SHARE, share))\n}\n\nexport function shareAlong(pointer: number, start: number, length: number, fallback: number) {\n  if (length <= 0) return fallback\n  return clampShare(((pointer - start) / length) * 100)\n}\n\n/** Keys move the divider the way it looks: Up on the horizontal divider moves it up, which lowers the share of the top image. */\nexport function shareForKey(key: string, share: number, step: number) {\n  const moves: Record<string, number> = {\n    ArrowLeft: share - step,\n    ArrowUp: share - step,\n    ArrowRight: share + step,\n    ArrowDown: share + step,\n    PageUp: share + step * PAGE_STEP_MULTIPLE,\n    PageDown: share - step * PAGE_STEP_MULTIPLE,\n    Home: MIN_SHARE,\n    End: MAX_SHARE,\n  }\n  const next = moves[key]\n  return next === undefined ? null : clampShare(next)\n}\n\nexport function clipFor(variant: BeforeAfterVariant, share: number, cross: number) {\n  if (variant === \"curtain\") return `inset(0 0 ${MAX_SHARE - share}% 0)`\n  if (variant === \"lens\") return `circle(${LENS_RADIUS} at ${share}% ${cross}%)`\n  return `inset(0 ${MAX_SHARE - share}% 0 0)`\n}\n\n/** A demonstration, not a value change: it goes out and comes back to where it started. */\nexport function sweepFrames(share: number, reach: number) {\n  return [share, clampShare(share - reach), clampShare(share + reach), share]\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/before-after-state.ts",
      "content": "import type * as React from \"react\"\nimport { useEffect, useRef } from \"react\"\nimport { animate, useMotionValue, useReducedMotion, useTransform, type AnimationPlaybackControls } from \"motion/react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { clampShare, clipFor, shareAlong, sweepFrames, type BeforeAfterVariant } from \"@/registry/miseui/ui/before-after-geometry\"\n\nconst SWEEP_REACH = 14\nconst SWEEP_SECONDS = 1.4\n\nexport function useComparison(variant: BeforeAfterVariant, value: number | undefined, defaultValue: number, onValueChange: ((share: number) => void) | undefined) {\n  const reduceMotion = useReducedMotion() ?? false\n  const handleRef = useRef<HTMLDivElement>(null)\n  const running = useRef<AnimationPlaybackControls | null>(null)\n  const share = useMotionValue(clampShare(value ?? defaultValue))\n  const cross = useMotionValue(50)\n  const clip = useTransform([share, cross], ([s = 0, c = 0]) => clipFor(variant, s as number, c as number))\n\n  // Written to the DOM rather than state, so a drag does not re-render both versions on every frame.\n  const report = (next: number) => {\n    handleRef.current?.setAttribute(\"aria-valuenow\", String(Math.round(next)))\n    onValueChange?.(next)\n  }\n\n  const glideTo = (next: number) => {\n    running.current?.stop()\n    if (reduceMotion) share.set(next)\n    else running.current = animate(share, next, SPRING_DEFAULT)\n  }\n\n  const trackTo = (next: number) => {\n    running.current?.stop()\n    share.set(next)\n  }\n\n  useEffect(() => {\n    if (value === undefined) return\n    const next = clampShare(value)\n    if (next === share.get()) return\n    running.current?.stop()\n    if (reduceMotion) share.set(next)\n    else running.current = animate(share, next, SPRING_DEFAULT)\n    handleRef.current?.setAttribute(\"aria-valuenow\", String(Math.round(next)))\n  }, [value, reduceMotion, share])\n\n  useEffect(() => () => running.current?.stop(), [])\n\n  return { handleRef, running, share, cross, clip, reduceMotion, report, glideTo, trackTo }\n}\n\nexport type Comparison = ReturnType<typeof useComparison>\n\nexport function useSweepOnce(frameRef: React.RefObject<HTMLDivElement | null>, comparison: Comparison, enabled: boolean) {\n  const { running, share } = comparison\n  useEffect(() => {\n    const frame = frameRef.current\n    if (!enabled || !frame || typeof IntersectionObserver === \"undefined\") return\n    const view = new IntersectionObserver(([entry]) => {\n      if (!entry?.isIntersecting) return\n      view.disconnect()\n      running.current = animate(share, sweepFrames(share.get(), SWEEP_REACH), { duration: SWEEP_SECONDS, ease: \"easeInOut\" })\n    })\n    view.observe(frame)\n    return () => view.disconnect()\n  }, [enabled, frameRef, running, share])\n}\n\nexport function useFrameDrag(variant: BeforeAfterVariant, comparison: Comparison) {\n  const { share, cross, report, glideTo, trackTo } = comparison\n  const dragging = useRef(false)\n\n  const shareAt = (event: React.PointerEvent<HTMLDivElement>) => {\n    const rect = event.currentTarget.getBoundingClientRect()\n    if (variant === \"curtain\") return shareAlong(event.clientY, rect.top, rect.height, share.get())\n    return shareAlong(event.clientX, rect.left, rect.width, share.get())\n  }\n  const followCross = (event: React.PointerEvent<HTMLDivElement>) => {\n    const rect = event.currentTarget.getBoundingClientRect()\n    if (variant === \"lens\") cross.set(shareAlong(event.clientY, rect.top, rect.height, cross.get()))\n  }\n\n  return {\n    onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => {\n      if (event.button !== 0) return\n      event.currentTarget.setPointerCapture?.(event.pointerId)\n      dragging.current = true\n      const next = shareAt(event)\n      followCross(event)\n      glideTo(next)\n      report(next)\n    },\n    onPointerMove: (event: React.PointerEvent<HTMLDivElement>) => {\n      // A lens follows a mouse with no press, the way a loupe does; a finger has to be down.\n      const hovering = variant === \"lens\" && event.pointerType === \"mouse\"\n      if (!dragging.current && !hovering) return\n      const next = shareAt(event)\n      followCross(event)\n      trackTo(next)\n      report(next)\n    },\n    onPointerUp: () => {\n      dragging.current = false\n    },\n    onPointerCancel: () => {\n      dragging.current = false\n    },\n  }\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}