2025-08-20 04:12:49 -06:00

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,
};