189 lines
5.9 KiB
TypeScript
189 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import {
|
|
AnimatePresence,
|
|
HTMLMotionProps,
|
|
motion,
|
|
Transition,
|
|
} from 'motion/react';
|
|
import { ArrowRight } from 'lucide-react';
|
|
import { cn } from '@workspace/ui/lib/utils';
|
|
|
|
type InputButtonContextType = {
|
|
showInput: boolean;
|
|
setShowInput: React.Dispatch<React.SetStateAction<boolean>>;
|
|
transition: Transition;
|
|
id: string;
|
|
};
|
|
const InputButtonContext = React.createContext<
|
|
InputButtonContextType | undefined
|
|
>(undefined);
|
|
|
|
const useInputButton = (): InputButtonContextType => {
|
|
const context = React.useContext(InputButtonContext);
|
|
if (!context) {
|
|
throw new Error('useInputButton must be used within a InputButton');
|
|
}
|
|
return context;
|
|
};
|
|
|
|
type InputButtonProviderProps = React.ComponentProps<'div'> &
|
|
Partial<InputButtonContextType>;
|
|
|
|
function InputButtonProvider({
|
|
className,
|
|
transition = { type: 'spring', stiffness: 300, damping: 20 },
|
|
showInput,
|
|
setShowInput,
|
|
id,
|
|
...props
|
|
}: InputButtonProviderProps) {
|
|
const localId = React.useId();
|
|
const [localShowInput, setLocalShowInput] = React.useState(false);
|
|
|
|
return (
|
|
<InputButtonContext.Provider
|
|
value={{
|
|
showInput: showInput ?? localShowInput,
|
|
setShowInput: setShowInput ?? setLocalShowInput,
|
|
transition,
|
|
id: id ?? localId,
|
|
}}
|
|
>
|
|
<div
|
|
data-slot="input-button-provider"
|
|
className={cn(
|
|
'relative w-fit flex items-center justify-center h-10',
|
|
(showInput || localShowInput) && 'w-full max-w-[400px]',
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
</InputButtonContext.Provider>
|
|
);
|
|
}
|
|
|
|
type InputButtonProps = HTMLMotionProps<'div'>;
|
|
|
|
function InputButton({ className, ...props }: InputButtonProps) {
|
|
return (
|
|
<motion.div
|
|
data-slot="input-button"
|
|
className={cn('flex size-full', className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
type InputButtonActionProps = HTMLMotionProps<'button'>;
|
|
|
|
function InputButtonAction({ className, ...props }: InputButtonActionProps) {
|
|
const { transition, setShowInput, id } = useInputButton();
|
|
|
|
return (
|
|
<motion.button
|
|
data-slot="input-button-action"
|
|
className={cn(
|
|
'bg-background text-sm whitespace-nowrap disabled:pointer-events-none disabled:opacity-50 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive rounded-full border text-background-foreground cursor-pointer pl-4 pr-12 size-full font-medium',
|
|
className,
|
|
)}
|
|
layoutId={`input-button-action-${id}`}
|
|
transition={transition}
|
|
onClick={() => setShowInput((prev) => !prev)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
type InputButtonSubmitProps = HTMLMotionProps<'button'> & {
|
|
icon?: React.ElementType;
|
|
};
|
|
|
|
function InputButtonSubmit({
|
|
className,
|
|
children,
|
|
icon: Icon = ArrowRight,
|
|
...props
|
|
}: InputButtonSubmitProps) {
|
|
const { transition, showInput, setShowInput, id } = useInputButton();
|
|
|
|
return (
|
|
<motion.button
|
|
data-slot="input-button-submit"
|
|
layoutId={`input-button-submit-${id}`}
|
|
transition={transition}
|
|
className={cn(
|
|
"z-[1] [&_svg:not([class*='size-'])]:size-4 cursor-pointer disabled:pointer-events-none disabled:opacity-50 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap bg-primary hover:bg-primary/90 transition-colors text-primary-foreground rounded-full text-sm flex items-center justify-center font-medium absolute inset-y-1 right-1",
|
|
showInput ? 'px-4' : 'aspect-square',
|
|
className,
|
|
)}
|
|
onClick={() => setShowInput((prev) => !prev)}
|
|
{...props}
|
|
>
|
|
{showInput ? (
|
|
<motion.span
|
|
key="show-button"
|
|
initial={{ opacity: 0, scale: 0 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
{children}
|
|
</motion.span>
|
|
) : (
|
|
<motion.span
|
|
key="show-icon"
|
|
initial={{ opacity: 0, scale: 0 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<Icon className="size-4" />
|
|
</motion.span>
|
|
)}
|
|
</motion.button>
|
|
);
|
|
}
|
|
|
|
type InputButtonInputProps = React.ComponentProps<'input'>;
|
|
|
|
function InputButtonInput({ className, ...props }: InputButtonInputProps) {
|
|
const { transition, showInput, id } = useInputButton();
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{showInput && (
|
|
<div className="absolute inset-0 size-full flex items-center justify-center">
|
|
<motion.div
|
|
layoutId={`input-button-input-${id}`}
|
|
className="size-full flex items-center bg-background rounded-full relative"
|
|
transition={transition}
|
|
>
|
|
<input
|
|
data-slot="input-button-input"
|
|
className={cn(
|
|
'size-full selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground inset-0 pl-4 focus-visible:border-ring border focus-visible:ring-ring/50 focus-visible:ring-[3px] pr-32 py-2 text-sm bg-background rounded-full focus:outline-none absolute shrink-0 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive disabled:pointer-events-none disabled:cursor-not-allowed',
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|
|
|
|
export {
|
|
InputButton,
|
|
InputButtonProvider,
|
|
InputButtonAction,
|
|
InputButtonSubmit,
|
|
InputButtonInput,
|
|
useInputButton,
|
|
type InputButtonProps,
|
|
type InputButtonProviderProps,
|
|
type InputButtonActionProps,
|
|
type InputButtonSubmitProps,
|
|
type InputButtonInputProps,
|
|
};
|