{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "openlmis-search-input",
  "title": "Search Input",
  "description": "Search field that reports after a pause in typing, on Enter or when it loses focus, with a clear button, so a list updates once per search rather than on every keystroke.",
  "dependencies": [
    "lucide-react@^1",
    "@base-ui/react@^1"
  ],
  "registryDependencies": [
    "input-group"
  ],
  "files": [
    {
      "path": "registry/components/openlmis/search-input/search-input.tsx",
      "content": "\"use client\"\n\nimport { SearchIcon, XIcon } from \"lucide-react\"\nimport { useRef } from \"react\"\n\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput,\n} from \"@/components/ui/input-group\"\n\nimport { useDebouncedInput } from \"@/components/openlmis/search-input/use-debounced-input\"\n\ntype SearchInputProps = {\n  value: string\n  /** Called once typing pauses, on Enter, on blur, and at once when cleared. */\n  onValueChange: (value: string) => void\n  placeholder?: string\n  /** Accessible name, when the placeholder alone does not say what is searched. */\n  label?: string\n  clearLabel?: string\n  /** Milliseconds of quiet before a value is reported. */\n  delay?: number\n}\n\nexport function SearchInput({\n  value,\n  onValueChange,\n  placeholder = \"Search...\",\n  label,\n  clearLabel = \"Clear Search\",\n  delay,\n}: SearchInputProps) {\n  const { draft, commit, inputProps } = useDebouncedInput(\n    value,\n    onValueChange,\n    delay\n  )\n  const input = useRef<HTMLInputElement>(null)\n\n  return (\n    <InputGroup>\n      <InputGroupAddon>\n        <SearchIcon />\n      </InputGroupAddon>\n      <InputGroupInput\n        aria-label={label ?? placeholder}\n        placeholder={placeholder}\n        ref={input}\n        type=\"text\"\n        {...inputProps}\n      />\n      {draft && (\n        <InputGroupAddon align=\"inline-end\">\n          <InputGroupButton\n            aria-label={clearLabel}\n            onClick={() => {\n              commit(\"\")\n              // The button goes away once the field is empty, so focus returns to the field.\n              input.current?.focus()\n            }}\n            // Keeps the field focused, so its blur never sends the text being cleared.\n            onMouseDown={(event) => event.preventDefault()}\n            size=\"icon-xs\"\n          >\n            <XIcon />\n          </InputGroupButton>\n        </InputGroupAddon>\n      )}\n    </InputGroup>\n  )\n}",
      "type": "registry:component",
      "target": "components/openlmis/search-input/search-input.tsx"
    },
    {
      "path": "registry/components/openlmis/search-input/use-debounced-input.ts",
      "content": "\"use client\"\n\nimport {\n  type ChangeEvent,\n  type KeyboardEvent,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\"\n\n/** Draft for a text input that reports after a pause and otherwise follows the upstream value. */\nexport function useDebouncedInput(\n  value: string,\n  onValueChange: (value: string) => void,\n  delay = 300\n) {\n  const [draft, setDraft] = useState(value)\n  const [syncedValue, setSyncedValue] = useState(value)\n  const [isTyping, setIsTyping] = useState(false)\n  const timer = useRef<ReturnType<typeof setTimeout>>(undefined)\n  const latestOnValueChange = useRef(onValueChange)\n\n  useLayoutEffect(() => {\n    latestOnValueChange.current = onValueChange\n  })\n\n  // Upstream wins unless the user is mid-typing; an echo that only trims the draft keeps it.\n  if (value !== syncedValue) {\n    setSyncedValue(value)\n    if (!isTyping && value !== draft.trim()) setDraft(value)\n  }\n\n  // Leaving the page drops unsent typing; sending it would update a page already gone.\n  useEffect(() => () => clearTimeout(timer.current), [])\n\n  const emit = (next: string) => {\n    clearTimeout(timer.current)\n    setIsTyping(false)\n    latestOnValueChange.current(next)\n  }\n\n  const change = (next: string) => {\n    setDraft(next)\n    setIsTyping(true)\n    clearTimeout(timer.current)\n    timer.current = setTimeout(() => emit(next), delay)\n  }\n\n  const commit = (next: string) => {\n    setDraft(next)\n    emit(next)\n  }\n\n  // Leaving the field, e.g. to press Clear Filters, sends what was typed first so nothing arrives after it.\n  const flush = () => {\n    if (isTyping) emit(draft)\n  }\n\n  const inputProps = {\n    value: draft,\n    onChange: (event: ChangeEvent<HTMLInputElement>) =>\n      change(event.target.value),\n    onBlur: flush,\n    onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => {\n      // Enter that confirms an IME candidate is part of typing, not a search.\n      if (event.key === \"Enter\" && !event.nativeEvent.isComposing) {\n        commit(event.currentTarget.value)\n      }\n    },\n  }\n\n  return { draft, commit, inputProps }\n}",
      "type": "registry:hook",
      "target": "components/openlmis/search-input/use-debounced-input.ts"
    }
  ],
  "cssVars": {
    "light": {
      "destructive": "oklch(0.577 0.245 27.325)",
      "destructive-foreground": "oklch(0.985 0 0)",
      "success": "oklch(0.596 0.145 163.225)",
      "success-foreground": "oklch(0.985 0 0)",
      "warning": "oklch(0.666 0.179 58.318)",
      "warning-foreground": "oklch(0.985 0 0)",
      "info": "oklch(0.55 0.13 237)",
      "info-foreground": "oklch(0.985 0 0)"
    },
    "dark": {
      "destructive": "oklch(0.704 0.191 22.216)",
      "destructive-foreground": "oklch(0.21 0.04 25)",
      "success": "oklch(0.696 0.17 162.48)",
      "success-foreground": "oklch(0.21 0.04 163)",
      "warning": "oklch(0.769 0.188 70.08)",
      "warning-foreground": "oklch(0.22 0.04 60)",
      "info": "oklch(0.7 0.14 237)",
      "info-foreground": "oklch(0.2 0.04 240)"
    }
  },
  "meta": {
    "project": "openlmis",
    "height": "128px"
  },
  "categories": [
    "forms",
    "data-table"
  ],
  "type": "registry:component"
}
