{
  "name": "compare-slider",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "@diceui/use-as-ref",
    "@diceui/use-isomorphic-layout-effect",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/compare-slider.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  ChevronDownIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  ChevronUpIcon,\n} from \"lucide-react\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\n\nconst ROOT_NAME = \"CompareSlider\";\nconst BEFORE_NAME = \"CompareSliderBefore\";\nconst AFTER_NAME = \"CompareSliderAfter\";\nconst LABEL_NAME = \"CompareSliderLabel\";\nconst HANDLE_NAME = \"CompareSliderHandle\";\n\nconst PAGE_KEYS = [\"PageUp\", \"PageDown\"];\nconst ARROW_KEYS = [\"ArrowUp\", \"ArrowDown\", \"ArrowLeft\", \"ArrowRight\"];\n\ntype Interaction = \"hover\" | \"drag\";\ntype Orientation = \"horizontal\" | \"vertical\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef<typeof CompareSlider>;\n\nfunction clamp(value: number, min: number, max: number): number {\n  return Math.min(Math.max(value, min), max);\n}\n\ninterface StoreState {\n  value: number;\n  isDragging: boolean;\n}\n\ninterface Store {\n  subscribe: (callback: () => void) => () => void;\n  getState: () => StoreState;\n  setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;\n  notify: () => void;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStore<T>(\n  selector: (state: StoreState) => T,\n  ogStore?: Store | null,\n): T {\n  const contextStore = React.useContext(StoreContext);\n\n  const store = ogStore ?? contextStore;\n\n  if (!store) {\n    throw new Error(`\\`useStore\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n\n  const getSnapshot = React.useCallback(\n    () => selector(store.getState()),\n    [store, selector],\n  );\n\n  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface CompareSliderContextValue {\n  interaction: Interaction;\n  orientation: Orientation;\n}\n\nconst CompareSliderContext =\n  React.createContext<CompareSliderContextValue | null>(null);\n\nfunction useCompareSliderContext(consumerName: string) {\n  const context = React.useContext(CompareSliderContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface CompareSliderProps extends DivProps {\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (value: number) => void;\n  step?: number;\n  interaction?: Interaction;\n  orientation?: Orientation;\n}\n\nfunction CompareSlider(props: CompareSliderProps) {\n  const {\n    value: valueProp,\n    defaultValue = 50,\n    onValueChange,\n    step = 1,\n    interaction = \"drag\",\n    orientation = \"horizontal\",\n    className,\n    children,\n    ref,\n    onPointerMove: onPointerMoveProp,\n    onPointerUp: onPointerUpProp,\n    onPointerDown: onPointerDownProp,\n    onKeyDown: onKeyDownProp,\n    asChild,\n    ...rootProps\n  } = props;\n\n  const stateRef = useLazyRef<StoreState>(() => ({\n    value: clamp(valueProp ?? defaultValue, 0, 100),\n    isDragging: false,\n  }));\n  const listenersRef = useLazyRef(() => new Set<() => void>());\n  const onValueChangeRef = useAsRef(onValueChange);\n\n  const store = React.useMemo<Store>(() => {\n    return {\n      subscribe: (cb) => {\n        listenersRef.current.add(cb);\n        return () => listenersRef.current.delete(cb);\n      },\n      getState: () => stateRef.current,\n      setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => {\n        if (Object.is(stateRef.current[key], value)) return;\n        stateRef.current[key] = value;\n\n        if (key === \"value\") {\n          onValueChangeRef.current?.(value as number);\n        }\n\n        store.notify();\n      },\n      notify: () => {\n        for (const cb of listenersRef.current) {\n          cb();\n        }\n      },\n    };\n  }, [listenersRef, stateRef, onValueChangeRef]);\n\n  const rootRef = React.useRef<RootElement | null>(null);\n  const composedRef = useComposedRefs(ref, rootRef);\n  const isDraggingRef = React.useRef(false);\n\n  const propsRef = useAsRef({\n    onPointerMove: onPointerMoveProp,\n    onPointerUp: onPointerUpProp,\n    onPointerDown: onPointerDownProp,\n    onKeyDown: onKeyDownProp,\n    interaction,\n    orientation,\n    step,\n  });\n\n  const value = useStore((state) => state.value, store);\n\n  useIsomorphicLayoutEffect(() => {\n    if (valueProp !== undefined) {\n      store.setState(\"value\", clamp(valueProp, 0, 100));\n    }\n  }, [valueProp]);\n\n  const onPointerMove = React.useCallback(\n    (event: React.PointerEvent<RootElement>) => {\n      if (!isDraggingRef.current && propsRef.current.interaction === \"drag\") {\n        return;\n      }\n      if (!rootRef.current) return;\n\n      propsRef.current.onPointerMove?.(event);\n      if (event.defaultPrevented) return;\n\n      const rootRect = rootRef.current.getBoundingClientRect();\n      const isVertical = propsRef.current.orientation === \"vertical\";\n      const position = isVertical\n        ? event.clientY - rootRect.top\n        : event.clientX - rootRect.left;\n      const size = isVertical ? rootRect.height : rootRect.width;\n      const percentage = clamp((position / size) * 100, 0, 100);\n\n      store.setState(\"value\", percentage);\n    },\n    [propsRef, store],\n  );\n\n  const onPointerDown = React.useCallback(\n    (event: React.PointerEvent<RootElement>) => {\n      if (propsRef.current.interaction !== \"drag\") return;\n\n      propsRef.current.onPointerDown?.(event);\n      if (event.defaultPrevented) return;\n\n      event.currentTarget.setPointerCapture(event.pointerId);\n      isDraggingRef.current = true;\n      store.setState(\"isDragging\", true);\n    },\n    [store, propsRef],\n  );\n\n  const onPointerUp = React.useCallback(\n    (event: React.PointerEvent<RootElement>) => {\n      if (propsRef.current.interaction !== \"drag\") return;\n\n      propsRef.current.onPointerUp?.(event);\n      if (event.defaultPrevented) return;\n\n      event.currentTarget.releasePointerCapture(event.pointerId);\n      isDraggingRef.current = false;\n      store.setState(\"isDragging\", false);\n    },\n    [store, propsRef],\n  );\n\n  const onKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<RootElement>) => {\n      propsRef.current.onKeyDown?.(event);\n      if (event.defaultPrevented) return;\n\n      const currentValue = store.getState().value;\n      const isVertical = propsRef.current.orientation === \"vertical\";\n\n      if (event.key === \"Home\") {\n        event.preventDefault();\n        store.setState(\"value\", 0);\n      } else if (event.key === \"End\") {\n        event.preventDefault();\n        store.setState(\"value\", 100);\n      } else if (PAGE_KEYS.concat(ARROW_KEYS).includes(event.key)) {\n        event.preventDefault();\n\n        const isPageKey = PAGE_KEYS.includes(event.key);\n        const isSkipKey =\n          isPageKey || (event.shiftKey && ARROW_KEYS.includes(event.key));\n        const multiplier = isSkipKey ? 10 : 1;\n\n        let direction = 0;\n        if (isVertical) {\n          const isDecreaseKey = [\"ArrowUp\", \"PageUp\"].includes(event.key);\n          direction = isDecreaseKey ? -1 : 1;\n        } else {\n          const isDecreaseKey = [\"ArrowLeft\", \"PageUp\"].includes(event.key);\n          direction = isDecreaseKey ? -1 : 1;\n        }\n\n        const stepInDirection = propsRef.current.step * multiplier * direction;\n        const newValue = clamp(currentValue + stepInDirection, 0, 100);\n        store.setState(\"value\", newValue);\n      }\n    },\n    [store, propsRef],\n  );\n\n  const contextValue = React.useMemo<CompareSliderContextValue>(\n    () => ({\n      interaction,\n      orientation,\n    }),\n    [interaction, orientation],\n  );\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <StoreContext.Provider value={store}>\n      <CompareSliderContext.Provider value={contextValue}>\n        <RootPrimitive\n          role=\"slider\"\n          aria-orientation={orientation}\n          aria-valuemax={100}\n          aria-valuemin={0}\n          aria-valuenow={value}\n          data-slot=\"compare-slider\"\n          data-orientation={orientation}\n          {...rootProps}\n          ref={composedRef}\n          tabIndex={0}\n          className={cn(\n            \"relative isolate touch-none overflow-hidden transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n            orientation === \"horizontal\" ? \"w-full\" : \"h-full\",\n            className,\n          )}\n          onPointerDown={onPointerDown}\n          onPointerMove={onPointerMove}\n          onPointerUp={onPointerUp}\n          onPointerCancel={onPointerUp}\n          onKeyDown={onKeyDown}\n        >\n          {children}\n        </RootPrimitive>\n      </CompareSliderContext.Provider>\n    </StoreContext.Provider>\n  );\n}\n\ninterface CompareSliderBeforeProps extends DivProps {\n  label?: string;\n}\n\nfunction CompareSliderBefore(props: CompareSliderBeforeProps) {\n  const { className, children, style, label, asChild, ref, ...beforeProps } =\n    props;\n\n  const value = useStore((state) => state.value);\n  const { orientation } = useCompareSliderContext(BEFORE_NAME);\n\n  const labelId = React.useId();\n\n  const isVertical = orientation === \"vertical\";\n  const clipPath = isVertical\n    ? `inset(${value}% 0 0 0)`\n    : `inset(0 0 0 ${value}%)`;\n\n  const BeforePrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <BeforePrimitive\n      role=\"img\"\n      aria-labelledby={label ? labelId : undefined}\n      aria-hidden={label ? undefined : \"true\"}\n      data-slot=\"compare-slider-before\"\n      data-orientation={orientation}\n      {...beforeProps}\n      ref={ref}\n      className={cn(\"absolute inset-0 h-full w-full object-cover\", className)}\n      style={{\n        clipPath,\n        ...style,\n      }}\n    >\n      {children}\n      {label && (\n        <CompareSliderLabel id={labelId} side=\"before\">\n          {label}\n        </CompareSliderLabel>\n      )}\n    </BeforePrimitive>\n  );\n}\n\ninterface CompareSliderAfterProps extends DivProps {\n  label?: string;\n}\n\nfunction CompareSliderAfter(props: CompareSliderAfterProps) {\n  const { className, children, style, label, asChild, ref, ...afterProps } =\n    props;\n\n  const value = useStore((state) => state.value);\n  const { orientation } = useCompareSliderContext(AFTER_NAME);\n\n  const labelId = React.useId();\n\n  const isVertical = orientation === \"vertical\";\n  const clipPath = isVertical\n    ? `inset(0 0 ${100 - value}% 0)`\n    : `inset(0 ${100 - value}% 0 0)`;\n\n  const AfterPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <AfterPrimitive\n      role=\"img\"\n      aria-labelledby={label ? labelId : undefined}\n      aria-hidden={label ? undefined : \"true\"}\n      data-slot=\"compare-slider-after\"\n      data-orientation={orientation}\n      {...afterProps}\n      ref={ref}\n      className={cn(\"absolute inset-0 h-full w-full object-cover\", className)}\n      style={{\n        clipPath,\n        ...style,\n      }}\n    >\n      {children}\n      {label && (\n        <CompareSliderLabel id={labelId} side=\"after\">\n          {label}\n        </CompareSliderLabel>\n      )}\n    </AfterPrimitive>\n  );\n}\n\nfunction CompareSliderHandle(props: DivProps) {\n  const { className, children, style, asChild, ref, ...handleProps } = props;\n\n  const value = useStore((state) => state.value);\n  const { interaction, orientation } = useCompareSliderContext(HANDLE_NAME);\n\n  const isVertical = orientation === \"vertical\";\n\n  const HandlePrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <HandlePrimitive\n      role=\"presentation\"\n      aria-hidden=\"true\"\n      data-slot=\"compare-slider-handle\"\n      data-orientation={orientation}\n      {...handleProps}\n      ref={ref}\n      className={cn(\n        \"absolute z-50 flex items-center justify-center\",\n        isVertical\n          ? \"left-0 h-10 w-full -translate-y-1/2\"\n          : \"top-0 h-full w-10 -translate-x-1/2\",\n        interaction === \"drag\" && \"cursor-grab active:cursor-grabbing\",\n        className,\n      )}\n      style={{\n        [isVertical ? \"top\" : \"left\"]: `${value}%`,\n        ...style,\n      }}\n    >\n      {children ?? (\n        <>\n          <div\n            className={cn(\n              \"absolute bg-background\",\n              isVertical\n                ? \"top-1/2 h-1 w-full -translate-y-1/2\"\n                : \"left-1/2 h-full w-1 -translate-x-1/2\",\n            )}\n          />\n          {interaction === \"drag\" && (\n            <div className=\"z-50 flex aspect-square size-11 shrink-0 items-center justify-center rounded-full bg-background p-2 [&_svg]:size-4 [&_svg]:stroke-3 [&_svg]:text-muted-foreground [&_svg]:select-none\">\n              {isVertical ? (\n                <div className=\"flex flex-col items-center\">\n                  <ChevronUpIcon />\n                  <ChevronDownIcon />\n                </div>\n              ) : (\n                <div className=\"flex items-center\">\n                  <ChevronLeftIcon />\n                  <ChevronRightIcon />\n                </div>\n              )}\n            </div>\n          )}\n        </>\n      )}\n    </HandlePrimitive>\n  );\n}\n\ninterface CompareSliderLabelProps extends DivProps {\n  side?: \"before\" | \"after\";\n}\n\nfunction CompareSliderLabel(props: CompareSliderLabelProps) {\n  const { className, children, side, asChild, ref, ...labelProps } = props;\n\n  const { orientation } = useCompareSliderContext(LABEL_NAME);\n  const isVertical = orientation === \"vertical\";\n\n  const LabelPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <LabelPrimitive\n      ref={ref}\n      data-slot=\"compare-slider-label\"\n      className={cn(\n        \"absolute z-20 rounded-md border border-border bg-background/80 px-3 py-1.5 text-sm font-medium backdrop-blur-sm\",\n        isVertical\n          ? side === \"before\"\n            ? \"top-2 left-2\"\n            : \"bottom-2 left-2\"\n          : side === \"before\"\n            ? \"top-2 left-2\"\n            : \"top-2 right-2\",\n        className,\n      )}\n      {...labelProps}\n    >\n      {children}\n    </LabelPrimitive>\n  );\n}\n\nexport {\n  CompareSlider,\n  CompareSliderAfter,\n  CompareSliderBefore,\n  CompareSliderHandle,\n  CompareSliderLabel,\n  type CompareSliderProps,\n};\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}