{ "name": "swap", "type": "registry:ui", "files": [ { "path": "ui/swap.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 * 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\";\n\nfunction getDataState(swapped: boolean) {\n return swapped ? \"on\" : \"off\";\n}\n\ninterface StoreState {\n swapped: 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 \\`Swap\\``);\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 SwapProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {\n swapped?: boolean;\n defaultSwapped?: boolean;\n onSwappedChange?: (swapped: boolean) => void;\n activationMode?: \"click\" | \"hover\";\n animation?: \"fade\" | \"rotate\" | \"flip\" | \"scale\";\n disabled?: boolean;\n}\n\nfunction Swap(props: SwapProps) {\n const {\n swapped: swappedProp,\n defaultSwapped,\n onSwappedChange,\n activationMode = \"click\",\n animation = \"fade\",\n disabled,\n render,\n className,\n onClick: onClickProp,\n onMouseEnter: onMouseEnterProp,\n onMouseLeave: onMouseLeaveProp,\n onKeyDown: onKeyDownProp,\n ...rootProps\n } = props;\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef(() => ({\n swapped: swappedProp ?? defaultSwapped ?? false,\n }));\n\n const propsRef = useAsRef({\n activationMode,\n animation,\n disabled,\n onSwappedChange,\n onClick: onClickProp,\n onMouseEnter: onMouseEnterProp,\n onMouseLeave: onMouseLeaveProp,\n onKeyDown: onKeyDownProp,\n });\n\n const isClickMode = activationMode === \"click\";\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, value) => {\n if (Object.is(stateRef.current[key], value)) return;\n\n if (key === \"swapped\" && typeof value === \"boolean\") {\n stateRef.current.swapped = value;\n propsRef.current.onSwappedChange?.(value);\n } else {\n stateRef.current[key] = value;\n }\n\n store.notify();\n },\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n },\n };\n }, [listenersRef, stateRef, propsRef]);\n\n const swapped = useStore((state) => state.swapped, store);\n\n useIsomorphicLayoutEffect(() => {\n if (swappedProp !== undefined) {\n store.setState(\"swapped\", swappedProp);\n }\n }, [swappedProp]);\n\n const onToggle = React.useCallback(() => {\n if (propsRef.current.disabled) return;\n\n store.setState(\"swapped\", !store.getState().swapped);\n }, [store, propsRef]);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onClick?.(event);\n if (event.defaultPrevented || propsRef.current.activationMode !== \"click\")\n return;\n\n onToggle();\n },\n [propsRef, onToggle],\n );\n\n const onMouseEnter = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onMouseEnter?.(event);\n if (\n event.defaultPrevented ||\n activationMode !== \"hover\" ||\n propsRef.current.disabled\n )\n return;\n\n store.setState(\"swapped\", true);\n },\n [propsRef, activationMode, store],\n );\n\n const onMouseLeave = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onMouseLeave?.(event);\n if (\n event.defaultPrevented ||\n activationMode !== \"hover\" ||\n propsRef.current.disabled\n )\n return;\n\n store.setState(\"swapped\", false);\n },\n [propsRef, activationMode, store],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n propsRef.current.onKeyDown?.(event);\n if (\n event.defaultPrevented ||\n propsRef.current.activationMode !== \"click\" ||\n propsRef.current.disabled\n )\n return;\n\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onToggle();\n }\n },\n [propsRef, onToggle],\n );\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: isClickMode ? \"button\" : undefined,\n \"aria-pressed\": isClickMode ? swapped : undefined,\n \"aria-disabled\": disabled,\n tabIndex: isClickMode && !disabled ? 0 : undefined,\n className: cn(\n \"relative inline-flex cursor-pointer select-none items-center justify-center data-disabled:cursor-not-allowed data-disabled:opacity-50\",\n className,\n ),\n onClick,\n onMouseEnter,\n onMouseLeave,\n onKeyDown,\n },\n rootProps,\n ),\n render,\n state: {\n slot: \"swap\",\n animation,\n state: getDataState(swapped),\n ...(disabled && { disabled: \"\" }),\n },\n });\n\n return {element};\n}\n\ninterface SwapOnProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {}\n\nfunction SwapOn({ render, className, ...props }: SwapOnProps) {\n const swapped = useStore((state) => state.swapped);\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"transition-all duration-300 data-[state=off]:absolute data-[state=off]:opacity-0 data-[state=on]:opacity-100 motion-reduce:transition-none\",\n \"[*[data-animation=rotate]_&]:data-[state=off]:rotate-180 [*[data-animation=rotate]_&]:data-[state=on]:rotate-0 motion-reduce:[*[data-animation=rotate]_&]:data-[state=off]:rotate-0\",\n \"[*[data-animation=flip]_&]:data-[state=off]:transform-[rotateY(180deg)] [*[data-animation=flip]_&]:data-[state=on]:transform-[rotateY(0deg)] motion-reduce:[*[data-animation=flip]_&]:data-[state=off]:transform-[rotateY(0deg)]\",\n \"[*[data-animation=scale]_&]:data-[state=off]:scale-0 [*[data-animation=scale]_&]:data-[state=on]:scale-100 motion-reduce:[*[data-animation=scale]_&]:data-[state=off]:scale-100\",\n className,\n ),\n },\n props,\n ),\n render,\n state: {\n slot: \"swap-on\",\n state: getDataState(swapped),\n },\n });\n}\n\ninterface SwapOffProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {}\n\nfunction SwapOff({ render, className, ...props }: SwapOffProps) {\n const swapped = useStore((state) => state.swapped);\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"transition-all duration-300 data-[state=on]:absolute data-[state=off]:opacity-100 data-[state=on]:opacity-0 motion-reduce:transition-none\",\n \"[*[data-animation=rotate]_&]:data-[state=off]:rotate-0 [*[data-animation=rotate]_&]:data-[state=on]:rotate-180 motion-reduce:[*[data-animation=rotate]_&]:data-[state=on]:rotate-0\",\n \"[*[data-animation=flip]_&]:data-[state=off]:transform-[rotateY(0deg)] [*[data-animation=flip]_&]:data-[state=on]:transform-[rotateY(180deg)] motion-reduce:[*[data-animation=flip]_&]:data-[state=on]:transform-[rotateY(0deg)]\",\n \"[*[data-animation=scale]_&]:data-[state=off]:scale-100 [*[data-animation=scale]_&]:data-[state=on]:scale-0 motion-reduce:[*[data-animation=scale]_&]:data-[state=on]:scale-100\",\n className,\n ),\n },\n props,\n ),\n render,\n state: {\n slot: \"swap-off\",\n state: getDataState(swapped),\n },\n });\n}\n\nexport { Swap, SwapOff, SwapOn, type SwapProps, useStore as useSwap };\n", "target": "" } ], "registryDependencies": [ "@diceui/use-as-ref", "@diceui/use-isomorphic-layout-effect", "@diceui/use-lazy-ref" ], "dependencies": [ "@base-ui/react" ] }