{ "name": "compare-slider", "type": "registry:ui", "files": [ { "path": "ui/compare-slider.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport {\n ChevronDownIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n ChevronUpIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useAsRef } from \"@/registry/bases/base/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/base/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/base/hooks/use-lazy-ref\";\nimport { useComposedRefs } from \"@/registry/bases/base/lib/compose-refs\";\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\ntype RootElement = React.ComponentRef;\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: (key: K, value: StoreState[K]) => void;\n notify: () => void;\n}\n\nconst StoreContext = React.createContext(null);\n\nfunction useStore(\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(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\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {\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 render,\n ref,\n onPointerMove: onPointerMoveProp,\n onPointerUp: onPointerUpProp,\n onPointerDown: onPointerDownProp,\n onKeyDown: onKeyDownProp,\n ...rootProps\n } = props;\n\n const stateRef = useLazyRef(() => ({\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(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n setState: (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(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) => {\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) => {\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) => {\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) => {\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(\n () => ({\n interaction,\n orientation,\n }),\n [interaction, orientation],\n );\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"slider\",\n \"aria-orientation\": orientation,\n \"aria-valuemax\": 100,\n \"aria-valuemin\": 0,\n \"aria-valuenow\": value,\n tabIndex: 0,\n className: cn(\n \"relative isolate touch-none select-none overflow-hidden outline-none transition-all 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,\n onPointerMove,\n onPointerUp,\n onPointerCancel: onPointerUp,\n onKeyDown,\n ref: composedRef,\n children,\n },\n rootProps,\n ),\n render,\n state: {\n slot: \"compare-slider\",\n orientation,\n },\n });\n\n return (\n \n \n {element}\n \n \n );\n}\n\ninterface CompareSliderBeforeProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {\n label?: string;\n}\n\nfunction CompareSliderBefore(props: CompareSliderBeforeProps) {\n const { className, children, style, label, render, 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 return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"img\",\n \"aria-labelledby\": label ? labelId : undefined,\n \"aria-hidden\": label ? undefined : \"true\",\n className: cn(\"absolute inset-0 h-full w-full object-cover\", className),\n style: {\n clipPath,\n ...style,\n },\n ref,\n children: (\n <>\n {children}\n {label && (\n \n {label}\n \n )}\n \n ),\n },\n beforeProps,\n ),\n render,\n state: {\n slot: \"compare-slider-before\",\n orientation,\n },\n });\n}\n\ninterface CompareSliderAfterProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {\n label?: string;\n}\n\nfunction CompareSliderAfter(props: CompareSliderAfterProps) {\n const { className, children, style, label, render, 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 return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"img\",\n \"aria-labelledby\": label ? labelId : undefined,\n \"aria-hidden\": label ? undefined : \"true\",\n className: cn(\"absolute inset-0 h-full w-full object-cover\", className),\n style: {\n clipPath,\n ...style,\n },\n ref,\n children: (\n <>\n {children}\n {label && (\n \n {label}\n \n )}\n \n ),\n },\n afterProps,\n ),\n render,\n state: {\n slot: \"compare-slider-after\",\n orientation,\n },\n });\n}\n\ninterface CompareSliderHandleProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {}\n\nfunction CompareSliderHandle(props: CompareSliderHandleProps) {\n const { className, children, style, render, 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 return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"presentation\",\n \"aria-hidden\": \"true\",\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 ref,\n children:\n children ??\n (() => (\n <>\n \n {interaction === \"drag\" && (\n
\n {isVertical ? (\n
\n \n \n
\n ) : (\n
\n \n \n
\n )}\n
\n )}\n \n ))(),\n },\n handleProps,\n ),\n render,\n state: {\n slot: \"compare-slider-handle\",\n orientation,\n },\n });\n}\n\ninterface CompareSliderLabelProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {\n side?: \"before\" | \"after\";\n}\n\nfunction CompareSliderLabel(props: CompareSliderLabelProps) {\n const { className, children, side, render, ref, ...labelProps } = props;\n\n const { orientation } = useCompareSliderContext(LABEL_NAME);\n const isVertical = orientation === \"vertical\";\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"absolute z-20 rounded-md border border-border bg-background/80 px-3 py-1.5 font-medium text-sm 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 ref,\n children,\n },\n labelProps,\n ),\n render,\n state: {\n slot: \"compare-slider-label\",\n },\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 = React.Ref | 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(ref: PossibleRef, 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(...refs: PossibleRef[]): React.RefCallback {\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(...refs: PossibleRef[]): React.RefCallback {\n // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", "target": "" } ], "registryDependencies": [ "@diceui/use-as-ref", "@diceui/use-isomorphic-layout-effect", "@diceui/use-lazy-ref" ], "dependencies": [ "@base-ui/react" ] }