{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-field",
  "title": "Text Field",
  "description": "The Material text field, filled and outlined, with the floating label, supporting text, and character counter.",
  "registryDependencies": [
    "https://guillermo-rebolledo.github.io/materialcn/r/field.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/input-group.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/materialcn-theme.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/ui/text-field.tsx",
      "content": "import { useId, useState, type ComponentProps, type ReactNode } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Field, FieldDescription, FieldError } from \"./field\"\nimport { InputGroup, InputGroupAddon, InputGroupInput, InputGroupTextarea } from \"./input-group\"\n\n/**\n * Material 3 text field.\n *\n * Kit geometry (Text fields page): 56dp tall, 16dp horizontal padding,\n * body-large content. The label lives *inside* the field and floats — onto\n * the outline as a 12/16 label in a Surface-filled notch for the outlined\n * style, or to the top of the tinted container for the filled style — when the\n * field is focused or holds a value.\n *\n * The outline carries the state: 1dp Outline at rest, 1dp On Surface on hover,\n * 3dp Primary when focused, 3dp Error when invalid. Extra weight is painted as\n * an inset shadow so the text never reflows. The filled style keeps only the\n * bottom \"active indicator\" (1dp → 3dp). Disabled fields tint the filled\n * container On Surface at 4% and leave the outlined one unfilled at 12%.\n */\ntype TextFieldVariant = \"outlined\" | \"filled\"\n\ntype TextFieldBaseProps = Omit<ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n  autoComplete?: string\n  disabled?: boolean\n  error?: string\n  invalid?: boolean\n  label: string\n  leading?: ReactNode\n  maxLength?: number\n  multiline?: boolean\n  name?: string\n  placeholder?: string\n  prefix?: ReactNode\n  readOnly?: boolean\n  rows?: number\n  suffix?: ReactNode\n  supportingText?: ReactNode\n  trailing?: ReactNode\n  variant?: TextFieldVariant\n}\n\ntype TextFieldValueProps =\n  | { defaultValue?: string; onValueChange?: (value: string) => void; value?: never }\n  | { defaultValue?: never; onValueChange: (value: string) => void; value: string }\n\ntype TextFieldProps = TextFieldBaseProps & TextFieldValueProps\n\n// Tailwind only generates classes it can read literally, so every variant\n// string below is spelled out rather than assembled.\nfunction TextField({\n  autoComplete,\n  className,\n  defaultValue = \"\",\n  disabled = false,\n  error,\n  invalid = false,\n  label,\n  leading,\n  maxLength,\n  multiline = false,\n  name,\n  onValueChange,\n  placeholder,\n  prefix,\n  readOnly = false,\n  rows = 3,\n  suffix,\n  supportingText,\n  trailing,\n  value: valueProp,\n  variant = \"outlined\",\n  ...props\n}: TextFieldProps) {\n  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue)\n  const value = valueProp ?? uncontrolledValue\n  const id = useId()\n  const errorId = `${id}-error`\n  const helpId = `${id}-help`\n  const describedBy = error ? errorId : supportingText || maxLength ? helpId : undefined\n  const showError = invalid || Boolean(error)\n  const setValue = (next: string) => {\n    if (valueProp === undefined) setUncontrolledValue(next)\n    onValueChange?.(next)\n  }\n  const controlProps = {\n    id,\n    name,\n    value,\n    disabled,\n    readOnly,\n    maxLength,\n    // A space keeps `:placeholder-shown` meaningful when no placeholder is\n    // given; a real placeholder is only revealed once the label has floated.\n    placeholder: placeholder || \" \",\n    autoComplete,\n    \"aria-invalid\": showError || undefined,\n    \"aria-describedby\": describedBy,\n    onChange: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setValue(event.target.value),\n  }\n  const filled = variant === \"filled\"\n  const controlClassName = cn(\n    \"placeholder:text-transparent focus:placeholder:text-m3-on-surface-variant\",\n    // Filled: the value sits under the floated label.\n    filled && !multiline && \"pt-5 pb-1\",\n    filled && multiline && \"pt-6\",\n    multiline && !filled && \"pt-5\",\n  )\n\n  return (\n    <Field\n      {...props}\n      data-slot=\"text-field\"\n      data-invalid={showError || undefined}\n      data-disabled={disabled || undefined}\n      data-readonly={readOnly || undefined}\n      data-variant={variant}\n      className={cn(\"gap-0\", className)}\n    >\n      <InputGroup\n        data-variant={variant}\n        data-populated={value.length > 0 || undefined}\n        className={cn(\n          \"text-foreground transition-[border-color,box-shadow,background-color] duration-(--m3-spring-effects-fast-duration) ease-(--m3-spring-effects-fast) motion-reduce:transition-none\",\n          filled\n            ? [\n                \"rounded-t-m3-xs bg-m3-surface-container-highest\",\n                // Active indicator: the 1dp bottom rule that grows to 3dp.\n                \"after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-m3-on-surface-variant\",\n                \"after:transition-[height,background-color] after:duration-(--m3-spring-effects-fast-duration) after:ease-(--m3-spring-effects-fast)\",\n                !disabled && \"hover:after:bg-m3-on-surface\",\n                \"has-[[data-slot=input-group-control]:focus-visible]:after:h-[3px] has-[[data-slot=input-group-control]:focus-visible]:after:bg-m3-primary\",\n                showError && \"after:h-[3px] after:bg-m3-error hover:after:bg-m3-error has-[[aria-invalid=true]]:after:bg-m3-error\",\n                disabled && \"bg-m3-on-surface/4 after:bg-m3-on-surface/38\",\n              ]\n            : [\n                \"rounded-m3-xs border border-m3-outline bg-transparent\",\n                !disabled && \"hover:border-m3-on-surface\",\n                \"has-[[data-slot=input-group-control]:focus-visible]:border-m3-primary has-[[data-slot=input-group-control]:focus-visible]:shadow-[inset_0_0_0_2px_var(--m3-primary)]\",\n                showError && \"border-m3-error shadow-[inset_0_0_0_2px_var(--m3-error)] hover:border-m3-error has-[[aria-invalid=true]]:border-m3-error has-[[aria-invalid=true]]:shadow-[inset_0_0_0_2px_var(--m3-error)]\",\n                disabled && \"border-m3-on-surface/12 hover:border-m3-on-surface/12\",\n              ],\n        )}\n      >\n        {leading && <InputGroupAddon align=\"inline-start\" className=\"min-h-14 px-3 [&>svg]:size-6\">{leading}</InputGroupAddon>}\n        {prefix && <InputGroupAddon align=\"inline-start\" className={cn(\"pl-3 text-m3-body-lg\", filled && \"pt-4\")}>{prefix}</InputGroupAddon>}\n        <label\n          htmlFor={id}\n          data-slot=\"text-field-label\"\n          className={cn(\n            \"pointer-events-none absolute z-10 max-w-[calc(100%-32px)] truncate text-m3-body-lg text-m3-on-surface-variant\",\n            \"transition-[top,left,font-size,line-height,color,translate] duration-(--m3-spring-effects-fast-duration) ease-(--m3-spring-effects-fast) motion-reduce:transition-none\",\n            // Resting position: at the text baseline, after any leading icon.\n            leading ? \"left-12\" : \"left-4\",\n            multiline ? \"top-4\" : \"top-1/2 -translate-y-1/2\",\n            // Floated position: 12/16 type, Primary while focused.\n            \"group-focus-within/input-group:text-m3-body-sm group-data-populated/input-group:text-m3-body-sm\",\n            \"group-focus-within/input-group:text-m3-primary\",\n            filled\n              ? \"group-focus-within/input-group:top-2 group-focus-within/input-group:translate-y-0 group-data-populated/input-group:top-2 group-data-populated/input-group:translate-y-0\"\n              : [\n                  // Outlined: a Surface-filled notch straddling the border.\n                  \"group-focus-within/input-group:top-0 group-focus-within/input-group:-translate-y-1/2 group-focus-within/input-group:left-3 group-focus-within/input-group:px-1 group-focus-within/input-group:bg-m3-surface\",\n                  \"group-data-populated/input-group:top-0 group-data-populated/input-group:-translate-y-1/2 group-data-populated/input-group:left-3 group-data-populated/input-group:px-1 group-data-populated/input-group:bg-m3-surface\",\n                ],\n            showError && \"text-m3-error group-focus-within/input-group:text-m3-error\",\n            disabled && \"text-m3-on-surface/38\",\n          )}\n        >\n          {label}\n        </label>\n        {multiline ? (\n          <InputGroupTextarea {...controlProps} rows={rows} className={controlClassName} />\n        ) : (\n          <InputGroupInput {...controlProps} className={controlClassName} />\n        )}\n        {suffix && <InputGroupAddon align=\"inline-end\" className={cn(\"pr-3 text-m3-body-lg\", filled && \"pt-4\")}>{suffix}</InputGroupAddon>}\n        {trailing && (\n          <InputGroupAddon align=\"inline-end\" className={cn(\"min-h-14 px-3 [&>svg]:size-6\", showError && \"text-m3-error\")}>\n            {trailing}\n          </InputGroupAddon>\n        )}\n      </InputGroup>\n      {(supportingText || maxLength) && (\n        <div id={helpId} className=\"flex justify-between gap-4 px-4 pt-1\">\n          {supportingText ? <FieldDescription className=\"text-m3-body-sm\">{supportingText}</FieldDescription> : <span />}\n          {maxLength && <span className=\"shrink-0 text-m3-body-sm text-muted-foreground\">{value.length} / {maxLength}</span>}\n        </div>\n      )}\n      {error && <FieldError id={errorId} className=\"px-4 pt-1 text-m3-body-sm\">{error}</FieldError>}\n    </Field>\n  )\n}\n\nexport { TextField, type TextFieldProps, type TextFieldVariant }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}