/* Copyright 2026 Marimo. All rights reserved. */ import { useControllableState } from "@radix-ui/react-use-controllable-state"; import { SearchIcon, XIcon } from "lucide-react"; import * as React from "react"; import { NumberField, type NumberFieldProps, } from "@/components/ui/number-field"; import { useDebounceControlledState } from "@/hooks/useDebounce"; import { cn } from "@/utils/cn"; import { Events } from "@/utils/events"; export type InputProps = React.InputHTMLAttributes & { rootClassName?: string; icon?: React.ReactNode; endAdornment?: React.ReactNode; }; const Input = React.forwardRef( ({ className, type, endAdornment, ...props }, ref) => { const icon = props.icon; if (type === "hidden") { return ; } return (
{icon && (
{icon}
)} {endAdornment && (
{endAdornment}
)}
); }, ); Input.displayName = "Input"; export const DebouncedInput = React.forwardRef< HTMLInputElement, InputProps & { value: string; onValueChange: (value: string) => void; delay?: number; } >(({ className, onValueChange, ...props }, ref) => { const { value, onChange } = useDebounceControlledState({ initialValue: props.value, delay: props.delay, onChange: onValueChange, }); return ( onChange(evt.target.value)} value={value} /> ); }); DebouncedInput.displayName = "DebouncedInput"; export const DebouncedNumberInput = React.forwardRef< HTMLInputElement, NumberFieldProps & { value: number; onValueChange: (valueAsNumber: number) => void; } >(({ className, onValueChange, ...props }, ref) => { // Create a debounced value of 200 const { value, onChange } = useDebounceControlledState({ initialValue: props.value, delay: 200, onChange: onValueChange, }); return ( ); }); DebouncedNumberInput.displayName = "DebouncedNumberInput"; export const SearchInput = React.forwardRef< HTMLInputElement, InputProps & { rootClassName?: string; icon?: React.ReactNode | null; clearable?: boolean; } >( ( { className, rootClassName, icon = , clearable = true, ...props }, ref, ) => { const uniqueId = React.useId(); const inputId = props.id || uniqueId; return (
{icon} {clearable && props.value && ( { e.preventDefault(); e.stopPropagation(); const input = document.getElementById(inputId); if (input && input instanceof HTMLInputElement) { input.focus(); input.value = ""; props.onChange?.({ ...e, target: input, currentTarget: input, type: "change", } as React.ChangeEvent); } }} > )}
); }, ); SearchInput.displayName = "SearchInput"; export const OnBlurredInput = React.forwardRef< HTMLInputElement, InputProps & { value: string; onValueChange: (value: string) => void; } >(({ className, onValueChange, ...props }, ref) => { const [internalValue, setInternalValue] = React.useState(props.value); const [value, setValue] = useControllableState({ prop: props.value, defaultProp: internalValue, onChange: onValueChange, }); React.useEffect(() => { setInternalValue(value || ""); }, [value]); return ( setInternalValue(event.target.value)} onBlur={() => setValue(internalValue || "")} onKeyDown={(event) => { if (event.key !== "Enter") { return; } setValue(internalValue || ""); }} /> ); }); OnBlurredInput.displayName = "OnBlurredInput"; export { Input };