{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"asset-input","type":"registry:ui","title":"Asset Input","description":"Labeled amount field for swaps, sells, and deposits, with slots for token selection, supporting text, balances, and actions. No wallet provider required.","dependencies":[],"devDependencies":[],"registryDependencies":["input","field"],"files":[{"path":"registry/onchain-ui/asset-input.tsx","type":"registry:ui","target":"components/ui/asset-input.tsx","content":"\"use client\";\n\nimport { useId, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { AmountInput, type AmountInputProps } from \"./amount-input\";\nimport { FieldSet, FieldLabel, FieldDescription, FieldError } from \"@/components/ui/field\";\n\nexport interface AssetInputProps extends Omit<AmountInputProps, \"className\"> {\n  /** Visible label connected to the amount input. */\n  label: ReactNode;\n  /** Token identity or your app's picker trigger. */\n  asset?: ReactNode;\n  /** Supporting text, such as a fiat estimate. Connected via aria-describedby. */\n  description?: ReactNode;\n  /** Validation message. Marks the input invalid and connects the message. */\n  error?: ReactNode;\n  /** Balance, Max actions, or other app-owned supporting content. */\n  footer?: ReactNode;\n  /** Applied to the field container. */\n  className?: string;\n  /** Applied to the amount input. */\n  inputClassName?: string;\n}\n\nexport function AssetInput({\n  label,\n  asset,\n  description,\n  error,\n  footer,\n  className,\n  inputClassName,\n  id,\n  disabled,\n  \"aria-describedby\": describedBy,\n  \"aria-invalid\": ariaInvalid,\n  ...props\n}: AssetInputProps) {\n  const generatedId = useId();\n  const inputId = id ?? generatedId;\n  const hasDescription = description != null && description !== false;\n  const hasError = error != null && error !== false && error !== \"\";\n  const invalid = hasError || ariaInvalid;\n  const descriptionIds = [\n    describedBy,\n    hasDescription ? `${inputId}-description` : undefined,\n    hasError ? `${inputId}-error` : undefined,\n  ].filter(Boolean).join(\" \") || undefined;\n\n  return (\n    <FieldSet\n      disabled={disabled}\n      data-slot=\"asset-input\"\n      data-invalid={invalid || undefined}\n      className={cn(\n        \"m-0 grid min-w-0 gap-3 rounded-xl border border-input bg-muted/30 p-4\",\n        \"focus-within:border-ring data-[invalid=true]:border-destructive disabled:opacity-50\",\n        className\n      )}\n    >\n      <FieldLabel htmlFor={inputId} className=\"text-sm font-medium text-muted-foreground\">\n        {label}\n      </FieldLabel>\n      <div className=\"flex min-w-0 items-center gap-3\">\n        {asset != null && <div className=\"min-w-0 max-w-[50%] shrink-0\">{asset}</div>}\n        <AmountInput\n          {...props}\n          id={inputId}\n          disabled={disabled}\n          aria-describedby={descriptionIds}\n          aria-invalid={invalid}\n          className={cn(\n            \"h-auto flex-1 rounded-sm border-0 bg-transparent px-0 py-1 text-right text-3xl tracking-tight shadow-none focus-visible:ring-2 disabled:opacity-100 md:text-3xl dark:bg-transparent\",\n            inputClassName\n          )}\n        />\n      </div>\n      {hasDescription && (\n        <FieldDescription id={`${inputId}-description`}>\n          {description}\n        </FieldDescription>\n      )}\n      {footer != null && <div className=\"flex min-w-0 flex-wrap items-center gap-2 text-xs text-muted-foreground\">{footer}</div>}\n      {hasError && (\n        <FieldError id={`${inputId}-error`}>\n          {error}\n        </FieldError>\n      )}\n    </FieldSet>\n  );\n}\n"},{"path":"registry/onchain-ui/amount-input.tsx","type":"registry:ui","target":"components/ui/amount-input.tsx","content":"\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Input } from \"@/components/ui/input\";\n\nexport interface AmountInputProps\n  extends Omit<\n    ComponentProps<\"input\">,\n    \"type\" | \"inputMode\" | \"value\" | \"defaultValue\" | \"onChange\" | \"min\" | \"max\" | \"step\"\n  > {\n  /** Unformatted decimal string. Empty and incomplete values are valid while editing. */\n  value: string;\n  /** Receives accepted edits without numeric conversion or rounding. */\n  onValueChange?: (value: string) => void;\n  /** Maximum fractional digits. Omit for unrestricted precision; 0 allows integers only. */\n  decimals?: number;\n}\n\n/** Checks editing syntax, not whether an amount is ready for a transaction. */\nexport function isAmountInputValue(value: string, decimals?: number): boolean {\n  if (decimals !== undefined && (!Number.isInteger(decimals) || decimals < 0)) {\n    return false;\n  }\n  if (!/^[0-9]*(\\.[0-9]*)?$/.test(value)) return false;\n  const point = value.indexOf(\".\");\n  return point === -1 || decimals === undefined ||\n    (decimals > 0 && value.length - point - 1 <= decimals);\n}\n\nexport function AmountInput({\n  value,\n  onValueChange,\n  decimals,\n  readOnly = !onValueChange,\n  disabled,\n  className,\n  placeholder = \"0\",\n  ...props\n}: AmountInputProps) {\n  return (\n    <Input\n      autoComplete=\"off\"\n      spellCheck={false}\n      size={1}\n      {...props}\n      data-slot=\"amount-input\"\n      type=\"text\"\n      inputMode=\"decimal\"\n      value={value}\n      placeholder={placeholder}\n      readOnly={readOnly}\n      disabled={disabled}\n      onChange={(event) => {\n        const next = event.target.value;\n        if (!disabled && !readOnly && isAmountInputValue(next, decimals)) {\n          onValueChange?.(next);\n        }\n      }}\n      className={cn(\n        \"font-mono tabular-nums\",\n        className\n      )}\n    />\n  );\n}\n"}]}