{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-field",
  "type": "registry:ui",
  "title": "Search field",
  "description": "A recognizable search entry point with clear affordances.",
  "categories": [
    "inputs"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/search-field/search-field.tsx",
      "content": "\"use client\";\nimport { forwardRef, useId, useRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { Search, X as Xmark } from \"lucide-react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\nexport interface SearchFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\"> {\n  label: string;\n  value: string;\n  onValueChange: (value: string) => void;\n}\nexport const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(function SearchField(\n  { label, value, onValueChange, id, className, ...props },\n  ref,\n) {\n  const generated = useId();\n  const controlId = id ?? generated;\n  const reduced = useReducedMotion();\n  const inputRef = useRef<HTMLInputElement | null>(null);\n  const setRefs = (node: HTMLInputElement | null) => {\n    inputRef.current = node;\n    if (typeof ref === \"function\") ref(node);\n    else if (ref) ref.current = node;\n  };\n  // Clearing returns focus to the field, since the clear button unmounts under the pointer.\n  function clear() {\n    onValueChange(\"\");\n    inputRef.current?.focus();\n  }\n  return (\n    <div className=\"grid min-w-0 gap-2\">\n      <label htmlFor={controlId} className=\"text-sm leading-body font-medium tracking-body\">\n        {label}\n      </label>\n      {/* Focus darkens the border; the shell never changes size. */}\n      <div\n        className=\"group/shell flex min-h-control-md items-center gap-2 rounded-control border border-line-strong bg-surface px-3 transition-[border-color] duration-fast ease-standard focus-within:border-ink hover:border-ink motion-reduce:transition-none\"\n        data-filled={value ? \"true\" : undefined}\n      >\n        {/* The glass wakes up with the field: muted at rest, full contrast while searching. */}\n        <Search\n          width={18}\n          height={18}\n          className=\"flex-[0_0_auto] text-ink-3 transition-[color] duration-fast ease-standard group-focus-within/shell:text-ink group-data-[filled=true]/shell:text-ink motion-reduce:transition-none\"\n          aria-hidden=\"true\"\n        />\n        <input\n          {...props}\n          ref={setRefs}\n          id={controlId}\n          type=\"search\"\n          value={value}\n          onChange={(event) => onValueChange(event.target.value)}\n          className={cn(\n            \"w-full min-w-0 border-0 bg-transparent text-(length:--text-sm) tracking-body text-ink outline-none placeholder:text-ink-3 [&::-webkit-search-cancel-button]:hidden\",\n            className,\n          )}\n        />\n        {/* The clear button has a reserved slot, so the field never changes width when it appears or leaves. */}\n        <span className=\"grid size-6 flex-[0_0_auto] place-items-center\">\n          <AnimatePresence initial={false}>\n            {value ? (\n              <motion.button\n                key=\"clear\"\n                type=\"button\"\n                className=\"grid size-6 flex-[0_0_auto] place-items-center rounded-button border-0 bg-transparent text-ink-3 [transition:background-color_var(--duration-fast)_var(--ease-standard),color_var(--duration-fast)_var(--ease-standard)] hover:bg-hover hover:text-ink motion-reduce:transition-none\"\n                tabIndex={0}\n                onClick={clear}\n                aria-label=\"Clear search\"\n                initial={\n                  reduced ? { opacity: 0 } : { opacity: 0, scale: 0.8, filter: `blur(${motionTokens.blur.subtle}px)` }\n                }\n                animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n                exit={\n                  reduced\n                    ? { opacity: 0, transition: { duration: 0 } }\n                    : {\n                        opacity: 0,\n                        scale: 0.8,\n                        filter: `blur(${motionTokens.blur.subtle}px)`,\n                        transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] },\n                      }\n                }\n                whileTap={\n                  reduced\n                    ? undefined\n                    : {\n                        scale: 0.96,\n                        transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] },\n                      }\n                }\n                transition={\n                  reduced\n                    ? { duration: motionTokens.duration.instant }\n                    : {\n                        ...motionTokens.spring.snappy,\n                        opacity: { duration: motionTokens.duration.fast },\n                        filter: { duration: motionTokens.duration.fast },\n                      }\n                }\n              >\n                <Xmark width={16} height={16} aria-hidden=\"true\" />\n              </motion.button>\n            ) : null}\n          </AnimatePresence>\n        </span>\n      </div>\n    </div>\n  );\n});\n",
      "type": "registry:component",
      "target": "@components/iris/search-field/search-field.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/search-field",
  "meta": {
    "kind": "component",
    "group": "Inputs",
    "export": "SearchField",
    "tags": [
      "field",
      "search"
    ],
    "docs": "https://irisui.dev/components/search-field"
  }
}
