{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-input",
  "type": "registry:ui",
  "title": "Text Input",
  "description": "Text Input is a single-line text entry for short, free-form data such as names, emails, or search terms.",
  "author": "Skrewww",
  "dependencies": [],
  "registryDependencies": [
    "@skrewww/form-field",
    "@skrewww/foundation"
  ],
  "docs": "Host requirements (assumed already present, not installed by this command): react, react-dom.",
  "files": [
    {
      "path": "components/ui/TextInput.tsx",
      "content": "/** @skrewww-component text-input */\n\"use client\";\n\nimport { forwardRef, useId } from \"react\";\nimport { FormField } from \"@/components/ui/FormField\";\nimport {\n  TextInputControl,\n  type TextInputControlProps,\n} from \"@/components/ui/TextInputControl\";\n\nexport type TextInputSize = TextInputControlProps[\"size\"];\n\nexport type TextInputProps = TextInputControlProps & {\n  label: string;\n  supportingText?: string;\n  error?: string;\n  hideLabel?: boolean;\n};\n\nexport const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(\n  {\n    label,\n    supportingText,\n    error,\n    hideLabel = false,\n    id: idProp,\n    required,\n    ...controlProps\n  },\n  ref,\n) {\n  return (\n    <FormField\n      label={label}\n      controlId={idProp}\n      required={required}\n      hideLabel={hideLabel}\n      supportingText={supportingText}\n      error={error}\n    >\n      {({ controlId, describedBy, invalid }) => (\n        <TextInputControl\n          ref={ref}\n          id={controlId}\n          required={required}\n          aria-invalid={invalid || undefined}\n          aria-describedby={describedBy}\n          {...controlProps}\n        />\n      )}\n    </FormField>\n  );\n});\n",
      "type": "registry:ui",
      "target": "~/components/ui/TextInput.tsx"
    },
    {
      "path": "components/ui/text-input.module.css",
      "content": ".controlWrap {\n  position: relative;\n  display: flex;\n  align-items: center;\n  width: 100%;\n}\n\n.input {\n  width: 100%;\n  border: 1px solid var(--semantic-border-default);\n  border-radius: var(--shape-radius-control);\n  /* TextInput wasn't in the originally-reported list of squircle consumers,\n     but it consumes --shape-radius-control via plain border-radius exactly\n     like Button/Checkbox/Drawer's close button — same bug, fixed the same\n     way, found while auditing the other 5. */\n  clip-path: var(--squircle-clip-path-control, none);\n  background-color: var(--surface-fill-control);\n  background-image: var(--component-surface-gradient-overlay);\n  backdrop-filter: var(--glass-backdrop-filter-sm, none);\n  color: var(--semantic-text-primary);\n  font-family: inherit;\n  line-height: 1.4;\n  transition:\n    border-color 0.15s ease,\n    box-shadow 0.15s ease;\n}\n\n.input::placeholder {\n  color: var(--semantic-text-disabled);\n}\n\n.input:hover:not(:disabled):not([aria-invalid=\"true\"]) {\n  border-color: var(--semantic-border-strong);\n}\n\n.input:focus {\n  outline: none;\n}\n\n.input:focus-visible {\n  border-color: var(--semantic-focus-ring);\n  box-shadow: 0 0 0 2px var(--semantic-focus-ring);\n}\n\n/* clip-path clips an outset box-shadow ring exactly like it clips outline\n   (same reasoning as Button/Checkbox) — an inset ring stays fully visible\n   since it never extends past the clip boundary. Squircle mode only; every\n   other shape mode keeps the outset ring above unchanged. */\n[data-skrewww-shape=\"squircle\"] .input:focus-visible {\n  box-shadow: inset 0 0 0 2px var(--semantic-focus-ring);\n}\n\n.input:disabled {\n  cursor: not-allowed;\n  pointer-events: none;\n  color: var(--semantic-text-disabled);\n  border-color: var(--semantic-border-disabled);\n  background-color: var(--semantic-surface-disabled);\n  opacity: var(--opacity-disabled);\n}\n\n.input:read-only:not(:disabled) {\n  background-color: var(--semantic-surface-subtle);\n  cursor: default;\n}\n\n.input[aria-invalid=\"true\"] {\n  border-color: var(--semantic-action-danger);\n}\n\n.input[aria-invalid=\"true\"]:focus-visible {\n  box-shadow: 0 0 0 2px var(--semantic-action-danger);\n}\n\n[data-skrewww-shape=\"squircle\"] .input[aria-invalid=\"true\"]:focus-visible {\n  box-shadow: inset 0 0 0 2px var(--semantic-action-danger);\n}\n\n.input[type=\"search\"]::-webkit-search-cancel-button {\n  -webkit-appearance: none;\n  appearance: none;\n  display: none;\n}\n\n.sizeSm {\n  min-height: var(--control-height-sm);\n  padding: 0 var(--control-padding-x-sm);\n  font-size: var(--control-font-size-sm);\n}\n\n.sizeMd {\n  min-height: var(--control-height-md);\n  padding: 0 var(--control-padding-x-md);\n  font-size: var(--control-font-size-md);\n}\n\n.sizeLg {\n  min-height: var(--control-height-lg);\n  padding: 0 var(--control-padding-x-lg);\n  font-size: var(--control-font-size-lg);\n}\n\n.withLeading {\n  padding-left: 2.25rem;\n}\n\n.withTrailing {\n  padding-right: 2.25rem;\n}\n\n.leading,\n.trailing {\n  position: absolute;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  color: var(--semantic-icon-muted);\n  pointer-events: none;\n}\n\n.leading {\n  left: 0.75rem;\n}\n\n.trailing {\n  right: 0.75rem;\n  pointer-events: auto;\n}\n\n.trailingAction {\n  position: absolute;\n  right: 0.25rem;\n  display: inline-flex;\n  align-items: center;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .input {\n    transition: none;\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/text-input.module.css"
    },
    {
      "path": "components/ui/TextInputControl.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, type InputHTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport styles from \"@/components/ui/text-input.module.css\";\n\nexport type TextInputControlSize = \"sm\" | \"md\" | \"lg\";\n\nexport type TextInputControlProps = Omit<\n  InputHTMLAttributes<HTMLInputElement>,\n  \"size\"\n> & {\n  size?: TextInputControlSize;\n  leadingIcon?: ReactNode;\n  trailingIcon?: ReactNode;\n  trailingAction?: ReactNode;\n};\n\nconst sizeClass: Record<TextInputControlSize, string> = {\n  sm: styles.sizeSm,\n  md: styles.sizeMd,\n  lg: styles.sizeLg,\n};\n\n/** Control-only single-line input — compose with FormField for label and validation. */\nexport const TextInputControl = forwardRef<HTMLInputElement, TextInputControlProps>(\n  function TextInputControl(\n    {\n      size = \"md\",\n      leadingIcon,\n      trailingIcon,\n      trailingAction,\n      className,\n      disabled,\n      required,\n      readOnly,\n      ...props\n    },\n    ref,\n  ) {\n    return (\n      <div className={cn(styles.controlWrap, className)}>\n        {leadingIcon ? (\n          <span className={styles.leading} aria-hidden=\"true\">\n            {leadingIcon}\n          </span>\n        ) : null}\n\n        <input\n          ref={ref}\n          className={cn(\n            styles.input,\n            sizeClass[size],\n            leadingIcon ? styles.withLeading : undefined,\n            trailingIcon || trailingAction ? styles.withTrailing : undefined,\n          )}\n          disabled={disabled}\n          readOnly={readOnly}\n          required={required}\n          aria-required={required || undefined}\n          {...props}\n        />\n\n        {trailingAction ? (\n          <span className={styles.trailingAction}>{trailingAction}</span>\n        ) : trailingIcon ? (\n          <span className={styles.trailing} aria-hidden=\"true\">\n            {trailingIcon}\n          </span>\n        ) : null}\n      </div>\n    );\n  },\n);\n",
      "type": "registry:ui",
      "target": "~/components/ui/TextInputControl.tsx"
    },
    {
      "path": "lib/cn.ts",
      "content": "export function cn(...classes: Array<string | false | null | undefined>): string {\n  return classes.filter(Boolean).join(\" \");\n}\n",
      "type": "registry:lib",
      "target": "~/lib/cn.ts"
    }
  ]
}
