{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radix-input",
  "title": "Radix Input",
  "description": "A shadcn/ui-compatible native input with a smooth Motion-powered caret.",
  "dependencies": ["motion"],
  "registryDependencies": ["utils"],
  "files": [
    {
      "path": "registry/ui/radix/input/index.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n} from \"motion/react\";\nimport {\n  type ChangeEvent,\n  type ComponentPropsWithRef,\n  type Ref,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nconst inputClassName =\n  \"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40\";\n\nconst FIREFOX_REGEX = /firefox|fxios/i;\nconst CHROME_REGEX = /chrome|chromium|crios/i;\nconst ANIMATED_CARET_TYPES = new Set([\n  \"text\",\n  \"search\",\n  \"url\",\n  \"tel\",\n  \"password\",\n]);\n\nexport type InputProps = ComponentPropsWithRef<\"input\">;\n\nfunction setRef<T>(ref: Ref<T> | undefined, value: T) {\n  if (typeof ref === \"function\") {\n    ref(value);\n  } else if (ref) {\n    ref.current = value;\n  }\n}\n\nfunction Input({\n  className,\n  ref,\n  type = \"text\",\n  value,\n  defaultValue,\n  onChange,\n  onBlur,\n  onFocus,\n  ...props\n}: InputProps) {\n  const [internalValue, setInternalValue] = useState(\n    String(defaultValue ?? \"\")\n  );\n  const containerRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const caretMarkerRef = useRef<HTMLSpanElement>(null);\n  const caretX = useMotionValue(0);\n  const caretOpacity = useMotionValue(0);\n  const caretInitializedRef = useRef(false);\n  const prefersReducedMotion = useReducedMotion();\n  const isFileInput = type === \"file\";\n  const hasAnimatedCaret = ANIMATED_CARET_TYPES.has(type);\n  const isControlled = value !== undefined;\n  let renderedValue: InputProps[\"value\"];\n  if (isControlled && !isFileInput) {\n    renderedValue = value;\n  }\n  const inputValue = isControlled ? String(value) : internalValue;\n  const springCaretX = useSpring(\n    caretX,\n    prefersReducedMotion\n      ? { stiffness: 10_000, damping: 100, mass: 0.1 }\n      : { stiffness: 500, damping: 30, mass: 0.5 }\n  );\n\n  const setInputRef = useCallback(\n    (node: HTMLInputElement | null) => {\n      inputRef.current = node;\n      setRef(ref, node);\n    },\n    [ref]\n  );\n\n  const updateCaretFromInput = useCallback(\n    // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: caret mapping mirrors native selection and horizontal scroll edge cases.\n    (target: HTMLInputElement) => {\n      const measureSpan = measureRef.current;\n      const caretMarker = caretMarkerRef.current;\n      if (!(measureSpan && caretMarker) || target.selectionStart === null) {\n        caretOpacity.set(0);\n        return;\n      }\n\n      const styles = window.getComputedStyle(target);\n      const isPassword = target.type === \"password\";\n      const passwordChar = FIREFOX_REGEX.test(navigator.userAgent)\n        ? \"\\u25cf\"\n        : \"\\u2022\";\n      let fontSize = styles.fontSize;\n\n      if (\n        passwordChar === \"\\u2022\" &&\n        isPassword &&\n        !CHROME_REGEX.test(navigator.userAgent)\n      ) {\n        fontSize = `${Number.parseFloat(fontSize) + 6.25}px`;\n      }\n\n      measureSpan.style.font = `${styles.fontStyle} ${styles.fontWeight} ${fontSize} ${styles.fontFamily}`;\n      measureSpan.style.letterSpacing = styles.letterSpacing;\n      measureSpan.style.fontFeatureSettings = styles.fontFeatureSettings;\n      measureSpan.style.fontVariationSettings = styles.fontVariationSettings;\n      measureSpan.style.direction = styles.direction;\n      measureSpan.style.unicodeBidi = styles.unicodeBidi;\n      measureSpan.style.textAlign = styles.textAlign;\n\n      const selectionStart = target.selectionStart ?? 0;\n      const selectionEnd = target.selectionEnd ?? 0;\n      const isRtl = styles.direction === \"rtl\";\n      const caretIndex =\n        selectionStart === selectionEnd ||\n        target.selectionDirection === \"backward\"\n          ? selectionStart\n          : selectionEnd;\n      const textBeforeCaret = isPassword\n        ? passwordChar.repeat(caretIndex)\n        : target.value.slice(0, caretIndex);\n\n      if (isRtl) {\n        const textAfterCaret = isPassword\n          ? passwordChar.repeat(target.value.length - caretIndex)\n          : target.value.slice(caretIndex);\n        measureSpan.replaceChildren(\n          document.createTextNode(textBeforeCaret),\n          caretMarker,\n          document.createTextNode(textAfterCaret)\n        );\n      } else {\n        const textNode = caretMarker.previousSibling;\n        if (textNode?.nodeType === Node.TEXT_NODE) {\n          textNode.textContent = textBeforeCaret;\n        } else {\n          measureSpan.replaceChildren(\n            document.createTextNode(textBeforeCaret),\n            caretMarker\n          );\n        }\n      }\n      caretMarker.style.display = \"inline-block\";\n      caretMarker.style.width = \"0px\";\n      const paddingLeft = Number.parseFloat(styles.paddingLeft) || 0;\n      const borderLeft = Number.parseFloat(styles.borderLeftWidth) || 0;\n      const borderRight = Number.parseFloat(styles.borderRightWidth) || 0;\n\n      if (isRtl) {\n        measureSpan.style.boxSizing = \"border-box\";\n        measureSpan.style.width = `${target.offsetWidth}px`;\n        measureSpan.style.paddingLeft = styles.paddingLeft;\n        measureSpan.style.paddingRight = styles.paddingRight;\n        measureSpan.style.borderLeftWidth = styles.borderLeftWidth;\n        measureSpan.style.borderRightWidth = styles.borderRightWidth;\n        measureSpan.style.borderStyle = \"solid\";\n        measureSpan.style.borderColor = \"transparent\";\n        measureSpan.style.overflow = \"hidden\";\n        measureSpan.scrollLeft = target.scrollLeft;\n\n        const inputRect = target.getBoundingClientRect();\n        const markerRect = caretMarker.getBoundingClientRect();\n        const caretPosition = markerRect.left - inputRect.left;\n        const minX = paddingLeft + borderLeft - 1;\n        const maxX =\n          target.clientWidth -\n          (Number.parseFloat(styles.paddingRight) || 0) +\n          borderRight;\n        const isCaretVisible =\n          caretPosition >= minX && caretPosition <= maxX + 1;\n        const hasSelection = selectionStart !== selectionEnd;\n\n        if (hasSelection) {\n          caretOpacity.set(0);\n          return;\n        }\n\n        if (!caretInitializedRef.current) {\n          springCaretX.jump(caretPosition);\n          caretInitializedRef.current = true;\n        }\n        caretX.set(caretPosition);\n        caretOpacity.set(isCaretVisible ? 1 : 0);\n        return;\n      }\n\n      measureSpan.style.width = \"\";\n      measureSpan.style.paddingLeft = \"\";\n      measureSpan.style.paddingRight = \"\";\n      measureSpan.style.border = \"\";\n      measureSpan.style.overflow = \"\";\n      const absoluteWidth =\n        (textBeforeCaret.length > 0 ? measureSpan.offsetWidth : -1) +\n        paddingLeft +\n        borderLeft;\n\n      const paddingRight = Number.parseFloat(styles.paddingRight) || 0;\n      const maxScroll = Math.max(0, target.scrollWidth - target.clientWidth);\n      const visibleRight =\n        target.scrollLeft + target.clientWidth - paddingRight;\n      const visibleLeft = target.scrollLeft + paddingLeft;\n\n      if (absoluteWidth > visibleRight) {\n        target.scrollLeft = Math.min(\n          absoluteWidth - target.clientWidth + paddingRight,\n          maxScroll\n        );\n      } else if (absoluteWidth < visibleLeft) {\n        target.scrollLeft = Math.max(0, absoluteWidth - paddingLeft);\n      }\n\n      const caretPosition = absoluteWidth - target.scrollLeft;\n      const maxX = target.clientWidth - paddingRight;\n      const minX = paddingLeft + borderLeft - 1;\n      const isCaretVisible = caretPosition >= minX && caretPosition <= maxX + 1;\n      const hasSelection = selectionStart !== selectionEnd;\n\n      if (hasSelection) {\n        caretOpacity.set(0);\n        return;\n      }\n\n      const nextCaretX = Math.min(caretPosition, maxX);\n      caretX.set(nextCaretX);\n      if (!caretInitializedRef.current) {\n        springCaretX.jump(nextCaretX);\n        caretInitializedRef.current = true;\n      }\n      caretOpacity.set(isCaretVisible ? 1 : 0);\n    },\n    [caretOpacity, caretX, springCaretX]\n  );\n\n  const updateCaretRef = useRef(updateCaretFromInput);\n  updateCaretRef.current = updateCaretFromInput;\n  const caretOpacityRef = useRef(caretOpacity);\n  caretOpacityRef.current = caretOpacity;\n\n  useEffect(() => {\n    const input = inputRef.current;\n    if (\n      input &&\n      document.activeElement === input &&\n      input.value === inputValue\n    ) {\n      updateCaretRef.current(input);\n    }\n  }, [inputValue]);\n\n  useEffect(() => {\n    const input = inputRef.current;\n    if (input && document.activeElement === input && input.type === type) {\n      updateCaretRef.current(input);\n    }\n  }, [type]);\n\n  useEffect(() => {\n    const input = inputRef.current;\n    const container = containerRef.current;\n    if (!(input && container)) {\n      return;\n    }\n\n    const updateCaretIfFocused = () => {\n      if (document.activeElement === input) {\n        updateCaretRef.current(input);\n      }\n    };\n\n    const handleSelectionChange = () => {\n      if (document.activeElement !== input) {\n        return;\n      }\n      requestAnimationFrame(() => {\n        if (document.activeElement === input) {\n          updateCaretRef.current(input);\n        }\n      });\n    };\n\n    document.addEventListener(\"selectionchange\", handleSelectionChange);\n    document.fonts.addEventListener(\"loadingdone\", updateCaretIfFocused);\n    document.fonts.ready.then(updateCaretIfFocused);\n    input.addEventListener(\"scroll\", updateCaretIfFocused);\n\n    const resizeObserver = new ResizeObserver(updateCaretIfFocused);\n    resizeObserver.observe(container);\n    updateCaretIfFocused();\n\n    return () => {\n      document.removeEventListener(\"selectionchange\", handleSelectionChange);\n      document.fonts.removeEventListener(\"loadingdone\", updateCaretIfFocused);\n      input.removeEventListener(\"scroll\", updateCaretIfFocused);\n      resizeObserver.disconnect();\n    };\n  }, []);\n\n  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n    if (!(isControlled || isFileInput)) {\n      setInternalValue(event.target.value);\n    }\n    onChange?.(event);\n    requestAnimationFrame(() => updateCaretRef.current(event.target));\n  };\n\n  return (\n    <div\n      className=\"relative grid w-full grid-cols-1 p-0\"\n      ref={containerRef}\n      style={{ caretColor: hasAnimatedCaret ? \"transparent\" : \"auto\" }}\n    >\n      <input\n        {...props}\n        className={cn(\n          inputClassName,\n          \"col-start-1 col-end-2 row-start-1 row-end-2 text-inherit\",\n          className\n        )}\n        data-slot=\"input\"\n        defaultValue={isControlled || isFileInput ? undefined : defaultValue}\n        onBlur={(event) => {\n          caretOpacityRef.current.set(0);\n          onBlur?.(event);\n        }}\n        onChange={handleChange}\n        onFocus={(event) => {\n          requestAnimationFrame(() => updateCaretRef.current(event.target));\n          onFocus?.(event);\n        }}\n        ref={setInputRef}\n        style={{\n          ...props.style,\n          ...(hasAnimatedCaret ? { caretColor: \"transparent\" } : {}),\n        }}\n        type={type}\n        value={renderedValue}\n      />\n      <span\n        aria-hidden\n        className=\"pointer-events-none invisible absolute top-0 left-0 whitespace-pre\"\n        ref={measureRef}\n      >\n        <span aria-hidden ref={caretMarkerRef} />\n      </span>\n      {hasAnimatedCaret && (\n        <motion.div\n          className=\"pointer-events-none absolute top-1/2 left-0 h-[0.9em] w-0.5 bg-primary\"\n          style={{\n            x: springCaretX,\n            y: \"-50%\",\n            opacity: caretOpacity,\n          }}\n        />\n      )}\n    </div>\n  );\n}\n\nexport { Input };\n",
      "type": "registry:ui",
      "target": "components/sora-ui/radix/input.tsx"
    }
  ],
  "meta": {
    "agentMetadata": {
      "intent": "Collect a single-line value from the user with native input semantics.",
      "role": "input",
      "a11yConstraints": [
        "Preserve native input semantics, keyboard editing, selection, and label association."
      ],
      "motionEngine": "Motion spring animates a mirror caret for text, search, url, tel, and password inputs; other types keep the native caret, with an instant reduced-motion fallback.",
      "compositionRules": [
        "Use a visible label and helper text when the field needs context."
      ],
      "compositionRecipes": ["Compose with radix/button for form submission."]
    },
    "demoProps": {
      "Input": {
        "placeholder": {
          "value": "Type something..."
        },
        "type": {
          "value": "text",
          "options": {
            "Text": "text",
            "Password": "password",
            "Email": "email",
            "Search": "search"
          }
        }
      }
    }
  },
  "type": "registry:ui"
}
