{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ds-input",
  "title": "Input UI",
  "description": "Extended input with icon prefix/suffix, clearable button, character counter with maxLength, loading spinner, i18n labels, and error state.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "@lema-ds/input",
    "@lema-ds/label",
    "@lema-ds/ui-i18n",
    "@lema-ds/ds-locale-provider"
  ],
  "files": [
    {
      "path": "components/ds/ds-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { XIcon, Loader2Icon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Input as InputRoot } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { UI_I18N, type UILocale } from \"@/lib/ui-i18n\"\nimport { useUILocale } from \"@/components/ui/ds-locale-provider\"\n\n// ── Types ──\n\nexport interface InputProps\n  extends\n    Omit<React.ComponentProps<\"input\">, \"size\">,\n    VariantProps<typeof inputWrapperVariants> {\n  size?: \"sm\" | \"md\" | \"lg\"\n  icon?: React.ReactNode\n  iconPlacement?: \"left\" | \"right\"\n  clearable?: boolean\n  showCount?: boolean\n  loading?: boolean\n  error?: string\n  label?: string\n  locale?: UILocale\n}\n\n// ── Variants ──\n\nexport const inputWrapperVariants = cva(\n  \"relative flex items-center transition-[color,box-shadow,background-color] focus-within:ring-3 focus-within:ring-ring/30 has-[aria-invalid=true]:ring-3 has-[aria-invalid=true]:ring-destructive/20\",\n  {\n    variants: {\n      size: {\n        sm: \"h-8\",\n        md: \"h-9\",\n        lg: \"h-10\",\n      },\n      rounded: {\n        none: \"rounded-none\",\n        light: \"rounded-lg\",\n        full: \"rounded-full\",\n      },\n      variant: {\n        default: \"bg-input/50\",\n        muted: \"bg-muted\",\n      },\n      bordered: {\n        true: \"border focus-within:border-ring has-[aria-invalid=true]:border-destructive\",\n        false: \"\",\n      },\n    },\n    defaultVariants: {\n      size: \"md\",\n      rounded: \"light\",\n      variant: \"default\",\n      bordered: true,\n    },\n  }\n)\n\n// ── Component ──\n\nfunction Input({\n  size = \"md\",\n  rounded,\n  variant: inputVariant = \"default\",\n  bordered = true,\n  icon,\n  iconPlacement = \"left\",\n  clearable = false,\n  showCount = false,\n  loading = false,\n  error,\n  label,\n  locale: localeProp,\n  className,\n  id,\n  maxLength,\n  value,\n  defaultValue,\n  ...props\n}: InputProps) {\n  const locale = useUILocale(localeProp)\n  const i18n = UI_I18N[locale]\n  const generatedId = React.useId()\n  const inputId = id ?? generatedId\n  const errorId = error ? `${inputId}-error` : undefined\n  const countId = showCount ? `${inputId}-count` : undefined\n\n  const [internalValue, setInternalValue] = React.useState(\n    defaultValue?.toString() ?? \"\"\n  )\n  const isControlled = value !== undefined\n  const currentValue = isControlled ? value.toString() : internalValue\n\n  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    if (!isControlled) setInternalValue(e.target.value)\n    props.onChange?.(e)\n  }\n\n  const handleClear = () => {\n    if (!isControlled) setInternalValue(\"\")\n    const nativeEvent = new Event(\"input\", { bubbles: true })\n    const fakeTarget = document.createElement(\"input\")\n    fakeTarget.value = \"\"\n    const syntheticEvent = {\n      ...nativeEvent,\n      target: fakeTarget,\n      currentTarget: fakeTarget,\n    } as unknown as React.ChangeEvent<HTMLInputElement>\n    props.onChange?.(syntheticEvent)\n    const input = document.getElementById(inputId) as HTMLInputElement | null\n    if (input) {\n      input.value = \"\"\n      input.focus()\n    }\n  }\n\n  const showClear = clearable && currentValue.length > 0 && !loading\n\n  const iconSize =\n    size === \"sm\"\n      ? \"[&>svg]:size-3.5\"\n      : size === \"lg\"\n        ? \"[&>svg]:size-5\"\n        : \"[&>svg]:size-4\"\n\n  const needsLeftPad = (icon && iconPlacement === \"left\") || loading\n  const needsRightPad = (icon && iconPlacement === \"right\") || showClear\n\n  const leftPad = needsLeftPad\n    ? size === \"sm\"\n      ? \"pl-8\"\n      : size === \"lg\"\n        ? \"pl-10\"\n        : \"pl-9\"\n    : \"\"\n  const rightPad = needsRightPad\n    ? size === \"sm\"\n      ? \"pr-8\"\n      : size === \"lg\"\n        ? \"pr-10\"\n        : \"pr-9\"\n    : \"\"\n\n  return (\n    <div data-slot=\"ds-input-wrapper\" className=\"flex flex-col gap-1.5\">\n      {label && (\n        <Label\n          htmlFor={inputId}\n          data-slot=\"ds-input-label\"\n          className={cn(\n            \"font-medium text-muted-foreground\",\n            size === \"sm\" && \"text-xs\",\n            size === \"md\" && \"text-sm\",\n            size === \"lg\" && \"text-base\"\n          )}\n        >\n          {label}\n        </Label>\n      )}\n      <div\n        className={cn(\n          inputWrapperVariants({\n            size,\n            rounded,\n            variant: inputVariant,\n            bordered,\n          }),\n          className\n        )}\n      >\n        {icon && iconPlacement === \"left\" && (\n          <span\n            data-slot=\"ds-input-icon-left\"\n            className={cn(\n              \"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-muted-foreground\",\n              iconSize\n            )}\n            aria-hidden=\"true\"\n          >\n            {icon}\n          </span>\n        )}\n        {loading && (\n          <span\n            data-slot=\"ds-input-spinner\"\n            className={cn(\n              \"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-muted-foreground\",\n              iconSize\n            )}\n            aria-hidden=\"true\"\n          >\n            <Loader2Icon className=\"animate-spin\" />\n          </span>\n        )}\n        <InputRoot\n          id={inputId}\n          data-slot=\"ds-input\"\n          value={isControlled ? value : undefined}\n          defaultValue={!isControlled ? defaultValue : undefined}\n          onChange={handleChange}\n          maxLength={maxLength}\n          aria-invalid={!!error}\n          aria-describedby={\n            [errorId, countId].filter(Boolean).join(\" \") || undefined\n          }\n          disabled={props.disabled || loading}\n          className={cn(\n            \"border-none bg-transparent ring-0 focus-visible:border-none focus-visible:ring-0\",\n            leftPad,\n            rightPad,\n            size === \"sm\" && \"h-full text-xs\",\n            size === \"md\" && \"h-full text-sm\",\n            size === \"lg\" && \"h-full text-base\"\n          )}\n          {...props}\n        />\n        {showClear && (\n          <button\n            type=\"button\"\n            data-slot=\"ds-input-clear\"\n            aria-label={i18n.input.clear}\n            onClick={handleClear}\n            className={cn(\n              \"absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground transition-colors hover:text-foreground\",\n              iconSize\n            )}\n          >\n            <XIcon />\n          </button>\n        )}\n        {icon && iconPlacement === \"right\" && !showClear && (\n          <span\n            data-slot=\"ds-input-icon-right\"\n            className={cn(\n              \"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground\",\n              iconSize\n            )}\n            aria-hidden=\"true\"\n          >\n            {icon}\n          </span>\n        )}\n      </div>\n      <div className=\"flex items-center justify-between\">\n        {error && (\n          <p\n            id={errorId}\n            data-slot=\"ds-input-error\"\n            className={cn(\n              \"text-destructive\",\n              size === \"sm\" ? \"text-xs\" : \"text-xs\"\n            )}\n            role=\"alert\"\n          >\n            {error}\n          </p>\n        )}\n        {showCount && maxLength && (\n          <p\n            id={countId}\n            data-slot=\"ds-input-count\"\n            className={cn(\n              \"ml-auto text-muted-foreground\",\n              size === \"sm\" ? \"text-xs\" : \"text-xs\"\n            )}\n            aria-live=\"polite\"\n          >\n            {currentValue.length}/{maxLength}\n          </p>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { Input }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
