a new start
This commit is contained in:
209
components/ui/shadcn-io/comparison/index.tsx
Normal file
209
components/ui/shadcn-io/comparison/index.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import { GripVerticalIcon } from 'lucide-react';
|
||||
import {
|
||||
type MotionValue,
|
||||
motion,
|
||||
useMotionValue,
|
||||
useSpring,
|
||||
useTransform,
|
||||
} from 'motion/react';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type HTMLAttributes,
|
||||
type MouseEventHandler,
|
||||
type ReactNode,
|
||||
type TouchEventHandler,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type ImageComparisonContextType = {
|
||||
sliderPosition: number;
|
||||
setSliderPosition: (pos: number) => void;
|
||||
motionSliderPosition: MotionValue<number>;
|
||||
mode: 'hover' | 'drag';
|
||||
};
|
||||
|
||||
const ImageComparisonContext = createContext<
|
||||
ImageComparisonContextType | undefined
|
||||
>(undefined);
|
||||
|
||||
const useImageComparisonContext = () => {
|
||||
const context = useContext(ImageComparisonContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useImageComparisonContext must be used within a ImageComparison'
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export type ComparisonProps = HTMLAttributes<HTMLDivElement> & {
|
||||
mode?: 'hover' | 'drag';
|
||||
onDragStart?: () => void;
|
||||
onDragEnd?: () => void;
|
||||
};
|
||||
|
||||
export const Comparison = ({
|
||||
className,
|
||||
mode = 'drag',
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
...props
|
||||
}: ComparisonProps) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const motionValue = useMotionValue(50);
|
||||
const motionSliderPosition = useSpring(motionValue, {
|
||||
bounce: 0,
|
||||
duration: 0,
|
||||
});
|
||||
const [sliderPosition, setSliderPosition] = useState(50);
|
||||
|
||||
const handleDrag = (domRect: DOMRect, clientX: number) => {
|
||||
if (!isDragging && mode === 'drag') {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = clientX - domRect.left;
|
||||
const percentage = Math.min(Math.max((x / domRect.width) * 100, 0), 100);
|
||||
motionValue.set(percentage);
|
||||
setSliderPosition(percentage);
|
||||
};
|
||||
|
||||
const handleMouseDrag: MouseEventHandler<HTMLDivElement> = (event) => {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerRect = event.currentTarget.getBoundingClientRect();
|
||||
|
||||
handleDrag(containerRect, event.clientX);
|
||||
};
|
||||
|
||||
const handleTouchDrag: TouchEventHandler<HTMLDivElement> = (event) => {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerRect = event.currentTarget.getBoundingClientRect();
|
||||
const touches = Array.from(event.touches);
|
||||
|
||||
handleDrag(containerRect, touches.at(0)?.clientX ?? 0);
|
||||
};
|
||||
|
||||
const handleDragStart = () => {
|
||||
if (mode === 'drag') {
|
||||
setIsDragging(true);
|
||||
onDragStart?.();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
if (mode === 'drag') {
|
||||
setIsDragging(false);
|
||||
onDragEnd?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageComparisonContext.Provider
|
||||
value={{ sliderPosition, setSliderPosition, motionSliderPosition, mode }}
|
||||
>
|
||||
<div
|
||||
aria-label="Comparison slider"
|
||||
aria-valuemax={100}
|
||||
aria-valuemin={0}
|
||||
aria-valuenow={sliderPosition}
|
||||
className={cn(
|
||||
'relative isolate w-full select-none overflow-hidden',
|
||||
className
|
||||
)}
|
||||
onMouseDown={handleDragStart}
|
||||
onMouseLeave={handleDragEnd}
|
||||
onMouseMove={handleMouseDrag}
|
||||
onMouseUp={handleDragEnd}
|
||||
onTouchEnd={handleDragEnd}
|
||||
onTouchMove={handleTouchDrag}
|
||||
onTouchStart={handleDragStart}
|
||||
role="slider"
|
||||
tabIndex={0}
|
||||
{...props}
|
||||
/>
|
||||
</ImageComparisonContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type ComparisonItemProps = ComponentProps<typeof motion.div> & {
|
||||
position: 'left' | 'right';
|
||||
};
|
||||
|
||||
export const ComparisonItem = ({
|
||||
className,
|
||||
position,
|
||||
...props
|
||||
}: ComparisonItemProps) => {
|
||||
const { motionSliderPosition } = useImageComparisonContext();
|
||||
const leftClipPath = useTransform(
|
||||
motionSliderPosition,
|
||||
(value) => `inset(0 0 0 ${value}%)`
|
||||
);
|
||||
const rightClipPath = useTransform(
|
||||
motionSliderPosition,
|
||||
(value) => `inset(0 ${100 - value}% 0 0)`
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
className={cn('absolute inset-0 h-full w-full object-cover', className)}
|
||||
role="img"
|
||||
style={{
|
||||
clipPath: position === 'left' ? leftClipPath : rightClipPath,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type ComparisonHandleProps = ComponentProps<typeof motion.div> & {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const ComparisonHandle = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComparisonHandleProps) => {
|
||||
const { motionSliderPosition, mode } = useImageComparisonContext();
|
||||
const left = useTransform(motionSliderPosition, (value) => `${value}%`);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'-translate-x-1/2 absolute top-0 z-50 flex h-full w-10 items-center justify-center',
|
||||
mode === 'drag' && 'cursor-grab active:cursor-grabbing',
|
||||
className
|
||||
)}
|
||||
role="presentation"
|
||||
style={{ left }}
|
||||
{...props}
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
<div className="-translate-x-1/2 absolute left-1/2 h-full w-1 bg-background" />
|
||||
{mode === 'drag' && (
|
||||
<div className="z-50 flex items-center justify-center rounded-sm bg-background px-0.5 py-1">
|
||||
<GripVerticalIcon className="h-4 w-4 select-none text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
1997
components/ui/shadcn-io/editor/index.tsx
Normal file
1997
components/ui/shadcn-io/editor/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user