{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auto-reload",
  "type": "registry:component",
  "title": "Auto Reload",
  "description": "Toggle + collapsible threshold/amount form for automatic credit reloading with locale-aware currency.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/auto-reload/auto-reload.tsx",
      "content": "\"use client\";\n\nimport {\n  type ChangeEvent,\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type ReactNode,\n  useCallback,\n  useId,\n  useMemo,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Button } from \"@vllnt/ui\";\nimport { Input } from \"@vllnt/ui\";\nimport { Switch } from \"@vllnt/ui\";\n\nconst CENTS_PER_UNIT = 100;\nconst DEFAULT_LOCALE = \"en-US\";\nconst DEFAULT_CURRENCY = \"USD\";\nconst DEFAULT_STEP_CENTS = 100;\n\n/**\n * Snapshot passed to {@link AutoReloadProps.onSave}.\n *\n * @public\n */\nexport type AutoReloadSavePayload = {\n  reloadAmountCents: number;\n  thresholdCents: number;\n};\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type AutoReloadLabels = {\n  /** Caption for the disabled banner. */\n  disabledFallback?: string;\n  /** Caption for the toggle headline. Defaults to `\"Auto-reload\"`. */\n  heading?: string;\n  /** Helper line under the toggle headline. Defaults to `\"Automatically reload credits when balance is low.\"`. */\n  helper?: string;\n  /** Helper line under the reload-amount input. */\n  reloadHelper?: string;\n  /** Caption for the reload-amount input. Defaults to `\"Reload amount\"`. */\n  reloadLabel?: string;\n  /** Caption for the save button. Defaults to `\"Save settings\"`. */\n  save?: string;\n  /** Caption for the save button while saving. Defaults to `\"Saving…\"`. */\n  saving?: string;\n  /** Helper line under the threshold input. */\n  thresholdHelper?: string;\n  /** Caption for the threshold input. Defaults to `\"Threshold\"`. */\n  thresholdLabel?: string;\n};\n\nconst DEFAULT_LABELS = {\n  disabledFallback: \"Auto-reload is unavailable for this account.\",\n  heading: \"Auto-reload\",\n  helper: \"Automatically reload credits when balance is low.\",\n  reloadHelper: \"Amount to add when the threshold is hit.\",\n  reloadLabel: \"Reload amount\",\n  save: \"Save settings\",\n  saving: \"Saving…\",\n  thresholdHelper: \"Reload when the balance drops below this.\",\n  thresholdLabel: \"Threshold\",\n} as const satisfies Required<AutoReloadLabels>;\n\n/**\n * Props for {@link AutoReload}.\n *\n * @public\n */\nexport type AutoReloadProps = {\n  /** Currency code (ISO 4217). Defaults to `\"USD\"`. */\n  currency?: string;\n  /** Override the symbol displayed inside the inputs (e.g. `\"€\"`). */\n  currencySymbol?: string;\n  /** Initial enabled state when uncontrolled. */\n  defaultEnabled?: boolean;\n  /** Initial reload-amount value (cents) when uncontrolled. */\n  defaultReloadAmountCents?: number;\n  /** Initial threshold value (cents) when uncontrolled. */\n  defaultThresholdCents?: number;\n  /** When true, the consumer cannot interact with the control. */\n  disabled?: boolean;\n  /** Caption rendered with `disabled`. */\n  disabledMessage?: ReactNode;\n  /** Controlled enabled state. */\n  enabled?: boolean;\n  /** When true, the save button renders as loading + disabled. */\n  isSaving?: boolean;\n  /** Localizable strings. */\n  labels?: AutoReloadLabels;\n  /** BCP-47 locale tag. Defaults to `\"en-US\"`. */\n  locale?: string;\n  /** Highest allowed amount (cents). */\n  maxAmountCents?: number;\n  /** Lowest allowed amount (cents). Defaults to `100`. */\n  minAmountCents?: number;\n  /** Fires when the user clicks save. */\n  onSave?: (payload: AutoReloadSavePayload) => void;\n  /** Fires when the toggle changes. */\n  onToggle?: (enabled: boolean) => void;\n  /** Controlled reload-amount value (cents). */\n  reloadAmountCents?: number;\n  /** Step granularity for the inputs (cents). Defaults to `100`. */\n  stepCents?: number;\n  /** Controlled threshold value (cents). */\n  thresholdCents?: number;\n} & ComponentPropsWithoutRef<\"div\">;\n\nfunction centsToValue(cents: number): string {\n  return (cents / CENTS_PER_UNIT).toFixed(2);\n}\n\nfunction valueToCents(value: string): number {\n  const parsed = Number.parseFloat(value);\n  if (Number.isNaN(parsed)) return 0;\n  return Math.round(parsed * CENTS_PER_UNIT);\n}\n\nconst CURRENCY_FORMATTER_CACHE = new Map<string, Intl.NumberFormat>();\nfunction getCurrencyFormatter(\n  locale: string,\n  currency: string,\n): Intl.NumberFormat {\n  const key = `${locale}|${currency}`;\n  let formatter = CURRENCY_FORMATTER_CACHE.get(key);\n  if (!formatter) {\n    formatter = new Intl.NumberFormat(locale, {\n      currency,\n      style: \"currency\",\n    });\n    CURRENCY_FORMATTER_CACHE.set(key, formatter);\n  }\n  return formatter;\n}\n\nfunction getCurrencySymbol(locale: string, currency: string): string {\n  const formatted = getCurrencyFormatter(locale, currency).format(0);\n  const symbol = formatted.replaceAll(/[\\d\\s,.]/g, \"\");\n  return symbol.length > 0 ? symbol : currency;\n}\n\ntype ReloadFormProps = {\n  currencyDisplay: string;\n  isSaving: boolean;\n  labels: Required<AutoReloadLabels>;\n  maxAmountCents?: number;\n  minAmountCents: number;\n  onSave: () => void;\n  reloadAmount: number;\n  reloadAmountId: string;\n  setReloadAmount: (value: number) => void;\n  setThreshold: (value: number) => void;\n  stepCents: number;\n  threshold: number;\n  thresholdId: string;\n};\n\nfunction ReloadFormFields({\n  currencyDisplay,\n  isSaving,\n  labels,\n  maxAmountCents,\n  minAmountCents,\n  onSave,\n  reloadAmount,\n  reloadAmountId,\n  setReloadAmount,\n  setThreshold,\n  stepCents,\n  threshold,\n  thresholdId,\n}: ReloadFormProps): ReactNode {\n  const handleThreshold = useCallback(\n    (event: ChangeEvent<HTMLInputElement>) => {\n      setThreshold(valueToCents(event.target.value));\n    },\n    [setThreshold],\n  );\n  const handleReload = useCallback(\n    (event: ChangeEvent<HTMLInputElement>) => {\n      setReloadAmount(valueToCents(event.target.value));\n    },\n    [setReloadAmount],\n  );\n  const step = (stepCents / CENTS_PER_UNIT).toFixed(2);\n  const min = (minAmountCents / CENTS_PER_UNIT).toFixed(2);\n  const max =\n    maxAmountCents === undefined\n      ? undefined\n      : (maxAmountCents / CENTS_PER_UNIT).toFixed(2);\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n        <NumericField\n          currencyDisplay={currencyDisplay}\n          helper={labels.thresholdHelper}\n          id={thresholdId}\n          label={labels.thresholdLabel}\n          max={max}\n          min={min}\n          onChange={handleThreshold}\n          step={step}\n          value={centsToValue(threshold)}\n        />\n        <NumericField\n          currencyDisplay={currencyDisplay}\n          helper={labels.reloadHelper}\n          id={reloadAmountId}\n          label={labels.reloadLabel}\n          max={max}\n          min={min}\n          onChange={handleReload}\n          step={step}\n          value={centsToValue(reloadAmount)}\n        />\n      </div>\n      <div>\n        <Button disabled={isSaving} onClick={onSave} type=\"button\">\n          {isSaving ? labels.saving : labels.save}\n        </Button>\n      </div>\n    </div>\n  );\n}\n\ntype NumericFieldProps = {\n  currencyDisplay: string;\n  helper?: string;\n  id: string;\n  label: string;\n  max?: string;\n  min?: string;\n  onChange: (event: ChangeEvent<HTMLInputElement>) => void;\n  step: string;\n  value: string;\n};\n\nfunction NumericField({\n  currencyDisplay,\n  helper,\n  id,\n  label,\n  max,\n  min,\n  onChange,\n  step,\n  value,\n}: NumericFieldProps): ReactNode {\n  return (\n    <div className=\"flex flex-col gap-1\">\n      <label className=\"text-sm font-medium text-foreground\" htmlFor={id}>\n        {label} ({currencyDisplay})\n      </label>\n      <Input\n        id={id}\n        inputMode=\"decimal\"\n        max={max}\n        min={min}\n        onChange={onChange}\n        step={step}\n        type=\"number\"\n        value={value}\n      />\n      {helper ? (\n        <p className=\"text-xs text-muted-foreground\">{helper}</p>\n      ) : null}\n    </div>\n  );\n}\n\ntype ToggleHeaderProps = {\n  enabled: boolean;\n  heading: string;\n  helper: string;\n  id: string;\n  onCheckedChange: (next: boolean) => void;\n};\n\nfunction ToggleHeader({\n  enabled,\n  heading,\n  helper,\n  id,\n  onCheckedChange,\n}: ToggleHeaderProps): ReactNode {\n  const helperId = `${id}-helper`;\n  return (\n    <header className=\"flex items-start justify-between gap-3\">\n      <div className=\"flex flex-col gap-1\">\n        <span className=\"text-sm font-semibold text-foreground\" id={id}>\n          {heading}\n        </span>\n        <span className=\"text-xs text-muted-foreground\" id={helperId}>\n          {helper}\n        </span>\n      </div>\n      <Switch\n        aria-describedby={helperId}\n        aria-labelledby={id}\n        checked={enabled}\n        onCheckedChange={onCheckedChange}\n      />\n    </header>\n  );\n}\n\ntype ControllerOptions = {\n  defaultEnabled: boolean;\n  defaultReloadAmountCents: number;\n  defaultThresholdCents: number;\n  enabled?: boolean;\n  onToggle?: (enabled: boolean) => void;\n  reloadAmountCents?: number;\n  thresholdCents?: number;\n};\n\ntype ControllerState = {\n  enabled: boolean;\n  handleToggle: (next: boolean) => void;\n  reloadAmount: number;\n  setReloadAmount: (value: number) => void;\n  setThreshold: (value: number) => void;\n  threshold: number;\n};\n\nfunction useAutoReloadController(options: ControllerOptions): ControllerState {\n  const {\n    defaultEnabled,\n    defaultReloadAmountCents,\n    defaultThresholdCents,\n    enabled: controlledEnabled,\n    onToggle,\n    reloadAmountCents: controlledReload,\n    thresholdCents: controlledThreshold,\n  } = options;\n  const [uncontrolledEnabled, setUncontrolledEnabled] =\n    useState(defaultEnabled);\n  const [threshold, setThreshold] = useState(defaultThresholdCents);\n  const [reloadAmount, setReloadAmount] = useState(defaultReloadAmountCents);\n  const enabled = controlledEnabled ?? uncontrolledEnabled;\n\n  const handleToggle = useCallback(\n    (next: boolean) => {\n      if (controlledEnabled === undefined) setUncontrolledEnabled(next);\n      onToggle?.(next);\n    },\n    [controlledEnabled, onToggle],\n  );\n\n  return {\n    enabled,\n    handleToggle,\n    reloadAmount: controlledReload ?? reloadAmount,\n    setReloadAmount,\n    setThreshold,\n    threshold: controlledThreshold ?? threshold,\n  };\n}\n\n/**\n * Toggle + collapsible configuration form for automatic credit reloading.\n * Composes {@link Switch}, {@link Input}, and {@link Button}. Renders a\n * disabled banner when the consumer passes `disabled` so the control can\n * advertise itself before the user has access (e.g. before subscribing).\n *\n * Values flow through the component in **minor units** (cents). Inputs\n * display the corresponding currency unit; consumers receive the cents\n * value from `onSave`.\n *\n * @example\n * ```tsx\n * <AutoReload\n *   enabled={settings.autoReloadEnabled}\n *   thresholdCents={settings.thresholdCents}\n *   reloadAmountCents={settings.reloadAmountCents}\n *   currency=\"EUR\"\n *   onToggle={handleToggle}\n *   onSave={handleSave}\n *   isSaving={isSaving}\n * />\n * ```\n *\n * @public\n */\ntype DisabledBannerProps = {\n  className?: string;\n  message: ReactNode;\n} & ComponentPropsWithoutRef<\"div\">;\n\nconst DisabledBanner = forwardRef<HTMLDivElement, DisabledBannerProps>(\n  ({ className, message, ...rest }, ref) => (\n    <div\n      aria-disabled=\"true\"\n      className={cn(\n        \"flex items-start gap-3 rounded-2xl border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground\",\n        className,\n      )}\n      ref={ref}\n      {...rest}\n    >\n      {message}\n    </div>\n  ),\n);\nDisabledBanner.displayName = \"AutoReload.DisabledBanner\";\n\ntype ActivePanelProps = {\n  className?: string;\n  controller: ControllerState;\n  currencyDisplay: string;\n  isSaving: boolean;\n  labels: Required<AutoReloadLabels>;\n  maxAmountCents?: number;\n  minAmountCents: number;\n  onSave: () => void;\n  reloadAmountId: string;\n  stepCents: number;\n  thresholdId: string;\n  toggleId: string;\n} & ComponentPropsWithoutRef<\"div\">;\n\nconst ActivePanel = forwardRef<HTMLDivElement, ActivePanelProps>(\n  (props, ref) => {\n    const {\n      className,\n      controller,\n      currencyDisplay,\n      isSaving,\n      labels,\n      maxAmountCents,\n      minAmountCents,\n      onSave,\n      reloadAmountId,\n      stepCents,\n      thresholdId,\n      toggleId,\n      ...rest\n    } = props;\n    return (\n      <div\n        className={cn(\n          \"flex flex-col gap-4 rounded-2xl border bg-background p-4\",\n          className,\n        )}\n        ref={ref}\n        {...rest}\n      >\n        <ToggleHeader\n          enabled={controller.enabled}\n          heading={labels.heading}\n          helper={labels.helper}\n          id={toggleId}\n          onCheckedChange={controller.handleToggle}\n        />\n        {controller.enabled ? (\n          <ReloadFormFields\n            currencyDisplay={currencyDisplay}\n            isSaving={isSaving}\n            labels={labels}\n            maxAmountCents={maxAmountCents}\n            minAmountCents={minAmountCents}\n            onSave={onSave}\n            reloadAmount={controller.reloadAmount}\n            reloadAmountId={reloadAmountId}\n            setReloadAmount={controller.setReloadAmount}\n            setThreshold={controller.setThreshold}\n            stepCents={stepCents}\n            threshold={controller.threshold}\n            thresholdId={thresholdId}\n          />\n        ) : null}\n      </div>\n    );\n  },\n);\nActivePanel.displayName = \"AutoReload.ActivePanel\";\n\ntype AutoReloadInternalState = {\n  controller: ControllerState;\n  currencyDisplay: string;\n  handleSave: () => void;\n  reloadAmountId: string;\n  resolvedLabels: Required<AutoReloadLabels>;\n  thresholdId: string;\n  toggleId: string;\n};\n\nfunction useAutoReloadInternals(\n  props: AutoReloadProps,\n): AutoReloadInternalState {\n  const {\n    currency = DEFAULT_CURRENCY,\n    currencySymbol,\n    defaultEnabled = false,\n    defaultReloadAmountCents = 2000,\n    defaultThresholdCents = 1000,\n    enabled: controlledEnabled,\n    labels,\n    locale = DEFAULT_LOCALE,\n    onSave,\n    onToggle,\n    reloadAmountCents: controlledReload,\n    thresholdCents: controlledThreshold,\n  } = props;\n  const resolvedLabels = useMemo(\n    () => ({ ...DEFAULT_LABELS, ...labels }),\n    [labels],\n  );\n  const toggleId = useId();\n  const thresholdId = useId();\n  const reloadAmountId = useId();\n  const controller = useAutoReloadController({\n    defaultEnabled,\n    defaultReloadAmountCents,\n    defaultThresholdCents,\n    enabled: controlledEnabled,\n    onToggle,\n    reloadAmountCents: controlledReload,\n    thresholdCents: controlledThreshold,\n  });\n  const currencyDisplay = currencySymbol ?? getCurrencySymbol(locale, currency);\n  const handleSave = useCallback(() => {\n    onSave?.({\n      reloadAmountCents: controller.reloadAmount,\n      thresholdCents: controller.threshold,\n    });\n  }, [controller.reloadAmount, controller.threshold, onSave]);\n  return {\n    controller,\n    currencyDisplay,\n    handleSave,\n    reloadAmountId,\n    resolvedLabels,\n    thresholdId,\n    toggleId,\n  };\n}\n\nexport const AutoReload = forwardRef<HTMLDivElement, AutoReloadProps>(\n  (props, ref) => {\n    const {\n      className,\n      disabled = false,\n      disabledMessage,\n      isSaving = false,\n      maxAmountCents,\n      minAmountCents = 100,\n      stepCents = DEFAULT_STEP_CENTS,\n    } = props;\n    const internals = useAutoReloadInternals(props);\n    if (disabled) {\n      return (\n        <DisabledBanner\n          className={className}\n          message={disabledMessage ?? internals.resolvedLabels.disabledFallback}\n          ref={ref}\n        />\n      );\n    }\n    return (\n      <ActivePanel\n        className={className}\n        controller={internals.controller}\n        currencyDisplay={internals.currencyDisplay}\n        isSaving={isSaving}\n        labels={internals.resolvedLabels}\n        maxAmountCents={maxAmountCents}\n        minAmountCents={minAmountCents}\n        onSave={internals.handleSave}\n        ref={ref}\n        reloadAmountId={internals.reloadAmountId}\n        stepCents={stepCents}\n        thresholdId={internals.thresholdId}\n        toggleId={internals.toggleId}\n      />\n    );\n  },\n);\nAutoReload.displayName = \"AutoReload\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
