{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cep-field",
  "title": "CEP Field",
  "description": "A Brazilian postal code field with masking, address lookup states, and rounded, pill, and compact variants.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "input",
    "label",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/cep-field.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { CircleAlert, LoaderCircle, MapPin, Search } from \"lucide-react\";\nimport {\n  type ChangeEvent,\n  type ComponentProps,\n  createContext,\n  type KeyboardEvent,\n  useContext,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const CEP_FIELD_VARIANTS = [\"rounded\", \"pill\", \"compact\"] as const;\nexport const CEP_FIELD_STATUSES = [\"idle\", \"loading\", \"found\", \"not-found\", \"error\"] as const;\n\nexport type CepFieldVariant = (typeof CEP_FIELD_VARIANTS)[number];\nexport type CepFieldStatus = (typeof CEP_FIELD_STATUSES)[number];\n\nconst CEP_LENGTH = 8;\n\nexport function normalizeCep(value: string) {\n  return value.replace(/\\D/g, \"\").slice(0, CEP_LENGTH);\n}\n\nexport function formatCep(value: string) {\n  const digits = normalizeCep(value);\n  return digits.length > 5 ? `${digits.slice(0, 5)}-${digits.slice(5)}` : digits;\n}\n\nexport type CepFieldProps = Omit<ComponentProps<\"div\">, \"onChange\" | \"defaultValue\"> & {\n  variant?: CepFieldVariant;\n  /** Lookup state owned by the application that resolves the address. */\n  status?: CepFieldStatus;\n  /** Unmasked digits. */\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Runs once when eight digits are entered, and again on Enter or the lookup button. */\n  onLookup?: (cep: string) => void;\n  disabled?: boolean;\n};\n\nfunction cepFieldChrome(variant: CepFieldVariant) {\n  const compact = variant === \"compact\";\n  return {\n    labelClass: compact ? \"mb-1 text-[11.5px] leading-4\" : \"mb-1.5 text-[12px] leading-4\",\n    controlClass: cn(\n      compact ? \"gap-0.5 p-0.5 pl-2\" : \"gap-1 p-1 pl-2.5\",\n      variant === \"pill\" ? \"rounded-full\" : compact ? \"rounded-xl\" : \"rounded-[14px]\",\n    ),\n    inputClass: compact\n      ? \"h-7 px-1.5 text-[12.5px] leading-4 md:text-[12.5px]\"\n      : \"h-8 px-2 text-[13px] leading-[18px] md:text-[13px]\",\n    buttonClass: compact\n      ? \"h-7 px-2.5 text-[12px] has-[>svg]:px-2.5\"\n      : \"h-8 px-3.5 text-[12.5px] has-[>svg]:px-3.5\",\n    messageClass: compact\n      ? \"mt-1.5 gap-1.5 px-0.5 text-[11.5px] leading-4\"\n      : \"mt-2 gap-2 px-1 text-[12px] leading-4\",\n    addressClass: compact ? \"mt-1.5 rounded-[10px] p-2.5\" : \"mt-2 rounded-xl p-3\",\n    iconClass: compact ? \"size-3\" : \"size-3.5\",\n  };\n}\n\ntype CepFieldContextValue = {\n  value: string;\n  status: CepFieldStatus;\n  disabled: boolean;\n  complete: boolean;\n  setValue: (value: string) => void;\n  lookup: () => void;\n  inputId: string;\n  messageId: string;\n  chrome: ReturnType<typeof cepFieldChrome>;\n};\n\nconst CepFieldContext = createContext<CepFieldContextValue | null>(null);\n\nfunction useCepField(name: string) {\n  const context = useContext(CepFieldContext);\n  if (!context) throw new Error(`${name} must be used within CepField`);\n  return context;\n}\n\nconst cepFieldVariants = cva(\"w-full text-foreground\", {\n  variants: { variant: { rounded: \"\", pill: \"\", compact: \"\" } },\n});\n\nexport function CepField({\n  variant = \"rounded\",\n  status = \"idle\",\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onLookup,\n  disabled = false,\n  children,\n  className,\n  ...props\n}: CepFieldProps) {\n  const inputId = useId();\n  const messageId = useId();\n  const [internalValue, setInternalValue] = useState(() => normalizeCep(defaultValue));\n  const lastLookup = useRef<string | null>(null);\n  const current = normalizeCep(value ?? internalValue);\n  const complete = current.length === CEP_LENGTH;\n\n  const lookup = (cep = current, force = true) => {\n    if (cep.length !== CEP_LENGTH || disabled || status === \"loading\") return;\n    if (!force && lastLookup.current === cep) return;\n    lastLookup.current = cep;\n    onLookup?.(cep);\n  };\n\n  const setValue = (raw: string) => {\n    const next = normalizeCep(raw);\n    if (value === undefined) setInternalValue(next);\n    onValueChange?.(next);\n    if (next.length < CEP_LENGTH) lastLookup.current = null;\n    lookup(next, false);\n  };\n\n  return (\n    <CepFieldContext.Provider\n      value={{\n        value: current,\n        status,\n        disabled,\n        complete,\n        setValue,\n        lookup: () => lookup(),\n        inputId,\n        messageId,\n        chrome: cepFieldChrome(variant),\n      }}\n    >\n      <div\n        data-slot=\"cep-field\"\n        data-variant={variant}\n        data-status={status}\n        className={cn(cepFieldVariants({ variant }), disabled && \"opacity-55\", className)}\n        {...props}\n        aria-busy={status === \"loading\"}\n      >\n        {children}\n      </div>\n    </CepFieldContext.Provider>\n  );\n}\n\nexport type CepFieldLabelProps = ComponentProps<\"label\">;\n\nexport function CepFieldLabel({ children = \"CEP\", className, ...props }: CepFieldLabelProps) {\n  const context = useCepField(\"CepFieldLabel\");\n  return (\n    <Label\n      data-slot=\"cep-field-label\"\n      className={cn(\n        \"block font-medium text-muted-foreground select-auto\",\n        context.chrome.labelClass,\n        className,\n      )}\n      {...props}\n      htmlFor={context.inputId}\n    >\n      {children}\n    </Label>\n  );\n}\n\nexport type CepFieldControlProps = ComponentProps<\"div\">;\n\nexport function CepFieldControl({ children, className, ...props }: CepFieldControlProps) {\n  const context = useCepField(\"CepFieldControl\");\n  const failed = context.status === \"not-found\" || context.status === \"error\";\n  const found = context.status === \"found\";\n  return (\n    <div\n      data-slot=\"cep-field-control\"\n      className={cn(\n        \"flex min-w-0 items-center border bg-card transition-[border-color,box-shadow] duration-[120ms] ease-out hover:border-border-strong focus-within:border-border-strong focus-within:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_18%,transparent)] motion-reduce:transition-none\",\n        context.chrome.controlClass,\n        failed\n          ? \"border-destructive/70 hover:border-destructive focus-within:border-destructive\"\n          : found\n            ? \"border-[color-mix(in_oklab,var(--success)_45%,var(--border))]\"\n            : \"border-border\",\n        className,\n      )}\n      {...props}\n    >\n      <MapPin\n        className={cn(\n          \"shrink-0 transition-colors duration-200 ease-out motion-reduce:transition-none\",\n          found ? \"text-success-ink\" : \"text-subtle-foreground\",\n          context.chrome.iconClass,\n        )}\n        strokeWidth={1.9}\n        aria-hidden=\"true\"\n      />\n      {children}\n    </div>\n  );\n}\n\nexport type CepFieldInputProps = Omit<\n  ComponentProps<\"input\">,\n  \"value\" | \"defaultValue\" | \"onChange\"\n>;\n\n/** Maps a caret after `count` digits to its index in the formatted CEP. */\nfunction caretAfter(formatted: string, count: number) {\n  if (count === 0) return 0;\n  let seen = 0;\n  for (let index = 0; index < formatted.length; index += 1) {\n    if (/\\d/.test(formatted[index] ?? \"\")) seen += 1;\n    if (seen === count) return index + 1;\n  }\n  return formatted.length;\n}\n\nexport function CepFieldInput({ className, disabled, onKeyDown, ...props }: CepFieldInputProps) {\n  const context = useCepField(\"CepFieldInput\");\n  const inputRef = useRef<HTMLInputElement>(null);\n  const pendingCaret = useRef<number | null>(null);\n\n  useLayoutEffect(() => {\n    const input = inputRef.current;\n    if (pendingCaret.current === null || !input || document.activeElement !== input) return;\n    const caret = caretAfter(input.value, pendingCaret.current);\n    input.setSelectionRange(caret, caret);\n    pendingCaret.current = null;\n  });\n\n  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n    const { value, selectionStart } = event.target;\n    pendingCaret.current = normalizeCep(value.slice(0, selectionStart ?? value.length)).length;\n    context.setValue(value);\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n    onKeyDown?.(event);\n    if (!event.defaultPrevented && event.key === \"Enter\" && !event.nativeEvent.isComposing) {\n      event.preventDefault();\n      context.lookup();\n    }\n  };\n\n  return (\n    <Input\n      inputMode=\"numeric\"\n      autoComplete=\"postal-code\"\n      placeholder=\"00000-000\"\n      data-slot=\"cep-field-input\"\n      className={cn(\n        \"min-w-0 flex-1 rounded-none border-0 bg-transparent py-0 font-medium tabular-nums shadow-none outline-none placeholder:font-normal placeholder:text-subtle-foreground focus-visible:ring-0 aria-invalid:ring-0 disabled:pointer-events-auto disabled:cursor-not-allowed disabled:opacity-100 dark:bg-transparent\",\n        context.chrome.inputClass,\n        className,\n      )}\n      {...props}\n      ref={inputRef}\n      id={context.inputId}\n      value={formatCep(context.value)}\n      onChange={handleChange}\n      onKeyDown={handleKeyDown}\n      disabled={context.disabled || disabled}\n      aria-invalid={context.status === \"not-found\" || context.status === \"error\" || undefined}\n      aria-describedby={context.messageId}\n    />\n  );\n}\n\nexport type CepFieldLookupProps = ComponentProps<\"button\">;\n\nexport function CepFieldLookup({\n  children,\n  className,\n  disabled,\n  onClick,\n  ...props\n}: CepFieldLookupProps) {\n  const context = useCepField(\"CepFieldLookup\");\n  const loading = context.status === \"loading\";\n  return (\n    <Button\n      type=\"button\"\n      variant=\"secondary\"\n      data-slot=\"cep-field-lookup\"\n      className={cn(\n        \"shrink-0 gap-1.5 rounded-full bg-secondary py-0 font-medium text-secondary-foreground transition-[scale,background-color,color] duration-[140ms] ease-out-quint hover:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))] focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.97] disabled:pointer-events-auto disabled:cursor-not-allowed disabled:text-subtle-foreground disabled:opacity-100 disabled:hover:bg-secondary motion-reduce:transition-none motion-reduce:active:scale-100\",\n        loading && \"cursor-progress\",\n        context.chrome.buttonClass,\n        className,\n      )}\n      {...props}\n      disabled={context.disabled || disabled || !context.complete || loading}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.lookup();\n      }}\n    >\n      {loading ? (\n        <LoaderCircle\n          className={cn(context.chrome.iconClass, \"animate-spin motion-reduce:animate-none\")}\n          aria-hidden=\"true\"\n        />\n      ) : (\n        <Search className={context.chrome.iconClass} strokeWidth={2} aria-hidden=\"true\" />\n      )}\n      {children ?? \"Buscar\"}\n    </Button>\n  );\n}\n\nconst defaultMessages: Partial<Record<CepFieldStatus, string>> = {\n  loading: \"Buscando endereço…\",\n  \"not-found\": \"Não encontramos esse CEP. Confira os números.\",\n  error: \"Não foi possível buscar o CEP agora. Preencha o endereço abaixo.\",\n};\n\nexport type CepFieldMessageProps = ComponentProps<\"p\">;\n\n/** A live region that speaks lookup progress and failures. */\nexport function CepFieldMessage({ children, className, ...props }: CepFieldMessageProps) {\n  const context = useCepField(\"CepFieldMessage\");\n  const failed = context.status === \"not-found\" || context.status === \"error\";\n  const content = children ?? defaultMessages[context.status];\n  return (\n    <p\n      data-slot=\"cep-field-message\"\n      className={cn(\n        \"flex items-start empty:hidden\",\n        failed ? \"text-destructive-ink\" : \"text-muted-foreground\",\n        context.chrome.messageClass,\n        className,\n      )}\n      {...props}\n      id={context.messageId}\n      role=\"status\"\n      aria-live=\"polite\"\n    >\n      {content ? (\n        <>\n          {failed ? (\n            <CircleAlert\n              className={cn(\"mt-px shrink-0\", context.chrome.iconClass)}\n              aria-hidden=\"true\"\n            />\n          ) : null}\n          <span\n            className={cn(\n              \"min-w-0 flex-1\",\n              context.status === \"loading\" && \"shimmer-text motion-reduce:text-muted-foreground\",\n            )}\n          >\n            {content}\n          </span>\n        </>\n      ) : null}\n    </p>\n  );\n}\n\nexport type CepFieldAddressProps = ComponentProps<\"address\">;\n\n/** Shows the resolved address once the lookup succeeds. */\nexport function CepFieldAddress({ className, ...props }: CepFieldAddressProps) {\n  const context = useCepField(\"CepFieldAddress\");\n  if (context.status !== \"found\") return null;\n  return (\n    <address\n      data-slot=\"cep-field-address\"\n      className={cn(\n        \"grid animate-in gap-0.5 bg-muted text-[12.5px] leading-[18px] text-muted-foreground not-italic duration-240 ease-out-quint fade-in-0 slide-in-from-top-1 motion-reduce:animate-none [&>:first-child]:font-medium [&>:first-child]:text-foreground\",\n        context.chrome.addressClass,\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/cep-field.tsx"
    }
  ],
  "categories": [
    "brazil",
    "forms"
  ],
  "type": "registry:ui"
}