{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "floating-label-input",
  "type": "registry:ui",
  "title": "Floating label input",
  "description": "Return an error message, or nothing when valid. Runs on submit and after the first error.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://ui.minidev.pro/r/button.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/floating-label-input.tsx",
      "type": "registry:ui",
      "target": "components/ui/floating-label-input.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { CircleAlertIcon, MailIcon, UserIcon } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\nconst SNAPPY = { type: \"spring\", bounce: 0.18, duration: 0.42 } as const\n\ntype FloatingLabelField = {\n  name: string\n  label: string\n  type?: string\n  multiline?: boolean\n  icon?: React.ReactNode\n  hint?: string\n  autoComplete?: string\n  defaultValue?: string\n  required?: boolean\n  /** Return an error message, or nothing when valid. Runs on submit and after the first error. */\n  validate?: (value: string) => string | undefined\n}\n\nconst FIELDS: FloatingLabelField[] = [\n  { name: \"name\", label: \"Full name\", icon: <UserIcon />, autoComplete: \"name\", defaultValue: \"Maya Chen\", required: true },\n  {\n    name: \"email\",\n    label: \"Work email\",\n    type: \"email\",\n    icon: <MailIcon />,\n    autoComplete: \"email\",\n    hint: \"We send your Lumen invite here.\",\n    required: true,\n    validate: (v) => (!v ? \"Enter your work email.\" : /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v) ? undefined : \"That email looks incomplete.\"),\n  },\n  { name: \"message\", label: \"What will Northwind use Lumen for?\", multiline: true, hint: \"Optional, a sentence or two is plenty.\" },\n]\n\n/**\n * A text field whose label rests inside the input and floats up onto the top\n * border on focus or once filled, on a snappy spring (instant with reduced\n * motion). Input and textarea variants, helper text, an error state wired to\n * aria-invalid and aria-describedby, and an optional leading icon. The label\n * is a real <label> bound to the field. Pass `label` for a single field;\n * without it the component renders a small sign-up form from `fields`.\n */\ntype FloatingLabelInputProps = {\n  /** The field label. When omitted, the `fields` form is rendered instead. */\n  label?: string\n  /** Render a textarea instead of an input. */\n  multiline?: boolean\n  rows?: number\n  type?: string\n  value?: string\n  defaultValue?: string\n  onChange?: (value: string) => void\n  /** Icon shown at the start of the field (inputs only). */\n  icon?: React.ReactNode\n  hint?: React.ReactNode\n  /** Error message. Marks the field invalid and replaces the hint. */\n  error?: React.ReactNode\n  disabled?: boolean\n  required?: boolean\n  id?: string\n  name?: string\n  autoComplete?: string\n  /** Fields of the sample form rendered when no `label` is passed. */\n  fields?: FloatingLabelField[]\n  /** Called with the form values when the sample form is submitted and valid. */\n  onSubmit?: (values: Record<string, string>) => void\n  className?: string\n}\n\nfunction FloatingLabelInput({\n  label,\n  multiline = false,\n  rows = 4,\n  type = \"text\",\n  value,\n  defaultValue = \"\",\n  onChange,\n  icon,\n  hint,\n  error,\n  disabled = false,\n  required = false,\n  id,\n  name,\n  autoComplete,\n  fields = FIELDS,\n  onSubmit,\n  className,\n}: FloatingLabelInputProps) {\n  if (label === undefined) return <SampleForm fields={fields} onSubmit={onSubmit} className={className} />\n  return (\n    <FloatingField\n      label={label}\n      multiline={multiline}\n      rows={rows}\n      type={type}\n      value={value}\n      defaultValue={defaultValue}\n      onChange={onChange}\n      icon={icon}\n      hint={hint}\n      error={error}\n      disabled={disabled}\n      required={required}\n      id={id}\n      name={name}\n      autoComplete={autoComplete}\n      className={cn(\"w-full max-w-sm\", className)}\n    />\n  )\n}\n\ntype FieldProps = Omit<FloatingLabelInputProps, \"fields\" | \"onSubmit\" | \"label\"> & { label: string; onBlur?: () => void }\n\nfunction FloatingField({\n  label,\n  multiline,\n  rows = 4,\n  type = \"text\",\n  value,\n  defaultValue = \"\",\n  onChange,\n  onBlur,\n  icon,\n  hint,\n  error,\n  disabled,\n  required,\n  id,\n  name,\n  autoComplete,\n  className,\n}: FieldProps) {\n  const reduce = useReducedMotion()\n  const auto = React.useId()\n  const fieldId = id ?? auto\n  const [inner, setInner] = React.useState(defaultValue)\n  const [focused, setFocused] = React.useState(false)\n  const [autofilled, setAutofilled] = React.useState(false)\n  const ref = React.useRef<HTMLInputElement & HTMLTextAreaElement>(null)\n  const v = value ?? inner\n  const floated = focused || v.length > 0 || autofilled\n  const withIcon = Boolean(icon) && !multiline\n  const invalid = Boolean(error)\n  const describedBy = error || hint ? `${fieldId}-help` : undefined\n\n  React.useEffect(() => {\n    // Browsers autofill without firing change; float the label so it never sits on the text.\n    const el = ref.current\n    if (!el) return\n    const check = () => {\n      try {\n        setAutofilled(el.matches(\":autofill\"))\n      } catch {\n        /* selector unsupported */\n      }\n    }\n    const t = window.setTimeout(check, 400)\n    el.addEventListener(\"animationstart\", check)\n    return () => {\n      window.clearTimeout(t)\n      el.removeEventListener(\"animationstart\", check)\n    }\n  }, [])\n\n  const restY = 12\n  const shared = {\n    id: fieldId,\n    name,\n    value: v,\n    disabled,\n    required,\n    autoComplete,\n    \"aria-invalid\": invalid || undefined,\n    \"aria-describedby\": describedBy,\n    onFocus: () => setFocused(true),\n    onBlur: () => {\n      setFocused(false)\n      onBlur?.()\n    },\n    onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n      if (value === undefined) setInner(e.target.value)\n      setAutofilled(false)\n      onChange?.(e.target.value)\n    },\n    className: cn(\n      \"peer block w-full min-w-0 rounded-lg border border-border bg-surface px-3 text-sm text-fg shadow-xs outline-none\",\n      \"transition-[border-color,box-shadow] duration-[140ms] ease-hairline\",\n      \"hover:border-border-strong\",\n      \"focus-visible:border-accent focus-visible:shadow-[0_0_0_3px_var(--accent-soft)]\",\n      \"aria-invalid:border-danger aria-invalid:focus-visible:shadow-[0_0_0_3px_color-mix(in_oklch,var(--danger)_14%,transparent)]\",\n      \"disabled:cursor-not-allowed disabled:bg-sunken disabled:text-fg-muted disabled:shadow-none\",\n      multiline ? \"min-h-28 resize-y py-3 leading-5\" : \"h-11\",\n      withIcon && \"pl-9\"\n    ),\n  }\n\n  return (\n    <div data-slot=\"floating-label-input\" className={cn(\"flex flex-col gap-1.5\", className)}>\n      <div className=\"relative\">\n        {multiline ? <textarea ref={ref} rows={rows} {...shared} /> : <input ref={ref} type={type} {...shared} />}\n        {withIcon ? (\n          <span\n            aria-hidden\n            className={cn(\n              \"pointer-events-none absolute top-0 left-3 flex h-11 items-center transition-colors duration-[70ms] [&_svg]:size-4\",\n              invalid ? \"text-danger\" : focused ? \"text-accent-fg\" : \"text-fg-subtle\"\n            )}\n          >\n            {icon}\n          </span>\n        ) : null}\n        <motion.label\n          htmlFor={fieldId}\n          initial={false}\n          animate={floated ? { x: 8, y: -10, scale: 0.85 } : { x: withIcon ? 32 : 8, y: restY, scale: 1 }}\n          transition={reduce ? { duration: 0 } : SNAPPY}\n          style={{ transformOrigin: \"0% 50%\" }}\n          className={cn(\n            \"pointer-events-none absolute top-0 left-0 max-w-[calc(100%-1.5rem)] truncate rounded-md px-1 text-sm leading-5 select-none\",\n            \"bg-[linear-gradient(to_bottom,transparent_45%,var(--surface)_45%)]\",\n            \"transition-colors duration-[70ms]\",\n            invalid ? \"text-danger\" : focused ? \"text-accent-fg\" : floated ? \"text-fg-muted\" : \"text-fg-subtle\",\n            disabled && \"bg-[linear-gradient(to_bottom,transparent_45%,var(--sunken)_45%)]\"\n          )}\n        >\n          {label}\n          {required ? (\n            <span aria-hidden className=\"ml-0.5 text-fg-subtle\">\n              *\n            </span>\n          ) : null}\n        </motion.label>\n      </div>\n      {error || hint ? (\n        <p id={describedBy} aria-live=\"polite\" className={cn(\"flex items-start gap-1.5 px-1 text-xs\", invalid ? \"text-danger\" : \"text-fg-subtle\")}>\n          {invalid ? <CircleAlertIcon aria-hidden className=\"mt-0.5 size-3.5 shrink-0\" /> : null}\n          <span>{error ?? hint}</span>\n        </p>\n      ) : null}\n    </div>\n  )\n}\n\nfunction SampleForm({ fields, onSubmit, className }: { fields: FloatingLabelField[]; onSubmit?: (values: Record<string, string>) => void; className?: string }) {\n  const [values, setValues] = React.useState<Record<string, string>>(() => Object.fromEntries(fields.map((f) => [f.name, f.defaultValue ?? \"\"])))\n  const [errors, setErrors] = React.useState<Record<string, string | undefined>>({})\n  const [sent, setSent] = React.useState(false)\n  const formRef = React.useRef<HTMLFormElement>(null)\n\n  const check = (f: FloatingLabelField, v: string) => f.validate?.(v) ?? (f.required && !v.trim() ? `Enter your ${f.label.toLowerCase()}.` : undefined)\n\n  return (\n    <form\n      ref={formRef}\n      noValidate\n      data-slot=\"floating-label-input\"\n      aria-label=\"Join the Lumen beta\"\n      className={cn(\"flex w-full max-w-sm flex-col gap-4\", className)}\n      onSubmit={(e) => {\n        e.preventDefault()\n        const next = Object.fromEntries(fields.map((f) => [f.name, check(f, values[f.name] ?? \"\")]))\n        setErrors(next)\n        const first = fields.find((f) => next[f.name])\n        if (first) {\n          formRef.current?.querySelector<HTMLElement>(`[name=\"${first.name}\"]`)?.focus()\n          setSent(false)\n          return\n        }\n        setSent(true)\n        onSubmit?.(values)\n      }}\n    >\n      <div className=\"flex flex-col gap-0.5\">\n        <p className=\"text-lg font-semibold tracking-tight text-fg\">Join the Lumen beta</p>\n        <p className=\"text-sm text-fg-muted\">Northwind gets early access to invoicing and payouts.</p>\n      </div>\n      {fields.map((f) => (\n        <FloatingField\n          key={f.name}\n          name={f.name}\n          label={f.label}\n          type={f.type}\n          multiline={f.multiline}\n          rows={3}\n          icon={f.icon}\n          hint={f.hint}\n          autoComplete={f.autoComplete}\n          required={f.required}\n          value={values[f.name] ?? \"\"}\n          error={errors[f.name]}\n          onChange={(v) => {\n            setValues((s) => ({ ...s, [f.name]: v }))\n            setSent(false)\n            if (errors[f.name]) setErrors((s) => ({ ...s, [f.name]: check(f, v) }))\n          }}\n        />\n      ))}\n      <div className=\"flex items-center justify-between gap-3 pt-1\">\n        <p aria-live=\"polite\" className={cn(\"text-xs\", sent ? \"text-success\" : \"text-fg-subtle\")}>\n          {sent ? \"Request sent. Watch your inbox.\" : \"Takes about a minute.\"}\n        </p>\n        <Button type=\"submit\">Request access</Button>\n      </div>\n    </form>\n  )\n}\n\nexport { FloatingLabelInput }\nexport type { FloatingLabelInputProps, FloatingLabelField }\n"
    }
  ]
}
