{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"floating-input","type":"registry:ui","title":"Floating Input","description":"A text field whose label floats up on focus, with an underline that grows from the center and an error that shakes.","dependencies":["motion"],"registryDependencies":["https://ui.mekaigames.com/r/theme.json"],"files":[{"path":"components/ui/floating-input.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { AnimatePresence, motion, useAnimate, useReducedMotion } from \"motion/react\";\nimport { useEffect, useId, useState, type ChangeEvent, type FocusEvent, type InputHTMLAttributes, type Ref } from \"react\";\n\ntype FloatingInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, \"value\" | \"defaultValue\" | \"size\"> & {\n  label: string;\n  value?: string;\n  defaultValue?: string;\n  /** true marks the field invalid; a string also shows it as the message. */\n  error?: boolean | string;\n  /** Helper text shown under the field while there is no error. */\n  hint?: string;\n  /** Accent for the floated label and the underline. */\n  color?: string;\n  ref?: Ref<HTMLInputElement>;\n  /** Classes for the wrapper. */\n  className?: string;\n};\n\nconst ERROR = \"#ff5d5d\";\n\nexport function FloatingInput({\n  label,\n  value,\n  defaultValue = \"\",\n  onChange,\n  onFocus,\n  onBlur,\n  error,\n  hint,\n  color = \"#ff784f\",\n  type = \"text\",\n  id,\n  disabled,\n  placeholder,\n  ref,\n  className = \"\",\n  ...rest\n}: FloatingInputProps) {\n  const autoId = useId();\n  const inputId = id ?? `${autoId}-input`;\n  const messageId = `${inputId}-message`;\n  const reduce = useReducedMotion();\n  const [inner, setInner] = useState(defaultValue);\n  const [focused, setFocused] = useState(false);\n  const [scope, animate] = useAnimate<HTMLDivElement>();\n\n  const current = value ?? inner;\n  const floated = focused || current.length > 0;\n  const invalid = Boolean(error);\n  const message = typeof error === \"string\" && error ? error : hint;\n  const tone = invalid ? ERROR : color;\n\n  // Shake whenever a new error arrives.\n  useEffect(() => {\n    if (!error || reduce || !scope.current) return;\n    animate(scope.current, { x: [0, -9, 8, -6, 4, -2, 0] }, { duration: 0.42, ease: \"easeOut\" });\n  }, [error, reduce, animate, scope]);\n\n  function handleChange(e: ChangeEvent<HTMLInputElement>) {\n    if (value === undefined) setInner(e.target.value);\n    onChange?.(e);\n  }\n\n  function handleFocus(e: FocusEvent<HTMLInputElement>) {\n    setFocused(true);\n    onFocus?.(e);\n  }\n\n  function handleBlur(e: FocusEvent<HTMLInputElement>) {\n    setFocused(false);\n    onBlur?.(e);\n  }\n\n  const labelColor = invalid ? ERROR : focused ? color : floated ? \"#b9b0a4\" : \"#857c71\";\n\n  return (\n    <div className={`w-full ${disabled ? \"opacity-50\" : \"\"} ${className}`}>\n      <div\n        ref={scope}\n        className={`relative h-14 overflow-hidden rounded-xl border bg-ink-900 transition-colors ${\n          invalid ? \"border-[#ff5d5d]/60\" : focused ? \"border-ink-500\" : \"border-ink-600 hover:border-ink-500\"\n        }`}\n      >\n        <input\n          {...rest}\n          ref={ref}\n          id={inputId}\n          type={type}\n          value={current}\n          disabled={disabled}\n          placeholder={placeholder}\n          onChange={handleChange}\n          onFocus={handleFocus}\n          onBlur={handleBlur}\n          aria-invalid={invalid || undefined}\n          aria-describedby={message ? messageId : undefined}\n          className={`peer h-full w-full bg-transparent pb-2 pl-4 pt-6 text-[15px] text-bone outline-none placeholder:text-transparent focus:placeholder:text-bone-dim disabled:cursor-not-allowed ${\n            invalid ? \"pr-11\" : \"pr-4\"\n          }`}\n        />\n        <motion.label\n          htmlFor={inputId}\n          className=\"pointer-events-none absolute left-4 top-[17px] origin-top-left select-none whitespace-nowrap text-[15px] leading-5\"\n          initial={false}\n          animate={{ y: floated ? -9 : 0, scale: floated ? 0.76 : 1, color: labelColor }}\n          transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 420, damping: 32 }}\n        >\n          {label}\n        </motion.label>\n        <motion.span\n          aria-hidden=\"true\"\n          className=\"absolute inset-x-0 bottom-0 h-[2px] origin-center\"\n          style={{ backgroundColor: tone }}\n          initial={false}\n          animate={{ scaleX: focused || invalid ? 1 : 0 }}\n          transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 260, damping: 28 }}\n        />\n        <AnimatePresence initial={false}>\n          {invalid ? (\n            <motion.span\n              key=\"alert\"\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute right-3.5 top-1/2 grid size-5 -translate-y-1/2 place-items-center rounded-full text-[11px] font-black text-ink-950\"\n              style={{ backgroundColor: ERROR }}\n              initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.3 }}\n              animate={{ opacity: 1, scale: 1 }}\n              exit={{ opacity: 0, scale: reduce ? 1 : 0.3 }}\n              transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 520, damping: 18 }}\n            >\n              !\n            </motion.span>\n          ) : null}\n        </AnimatePresence>\n      </div>\n      <div id={messageId} aria-live=\"polite\" className=\"min-h-0\">\n        <AnimatePresence mode=\"wait\" initial={false}>\n          {message ? (\n            <motion.p\n              key={`${invalid ? \"error\" : \"hint\"}:${message}`}\n              className={`px-1 pt-2 text-xs leading-relaxed ${invalid ? \"text-[#ff8a8a]\" : \"text-bone-dim\"}`}\n              initial={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}\n              transition={{ duration: reduce ? 0 : 0.16 }}\n            >\n              {message}\n            </motion.p>\n          ) : null}\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n"}]}