{
  "name": "key-value",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "button",
    "input",
    "textarea",
    "@diceui/use-as-ref",
    "@diceui/use-isomorphic-layout-effect",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/key-value.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { PlusIcon, XIcon } from \"lucide-react\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { VisuallyHiddenInput } from \"@/registry/bases/radix/components/visually-hidden-input\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\nimport { Button } from \"@/registry/bases/radix/ui/button\";\nimport { Input } from \"@/registry/bases/radix/ui/input\";\nimport { Textarea } from \"@/registry/bases/radix/ui/textarea\";\n\nconst ROOT_NAME = \"KeyValue\";\nconst LIST_NAME = \"KeyValueList\";\nconst ITEM_NAME = \"KeyValueItem\";\nconst KEY_INPUT_NAME = \"KeyValueKeyInput\";\nconst VALUE_INPUT_NAME = \"KeyValueValueInput\";\nconst REMOVE_NAME = \"KeyValueRemove\";\nconst ADD_NAME = \"KeyValueAdd\";\nconst ERROR_NAME = \"KeyValueError\";\n\ntype Orientation = \"vertical\" | \"horizontal\";\ntype Field = \"key\" | \"value\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef<typeof KeyValue>;\ntype KeyInputElement = React.ComponentRef<typeof KeyValueKeyInput>;\ntype RemoveElement = React.ComponentRef<typeof KeyValueRemove>;\ntype AddElement = React.ComponentRef<typeof KeyValueAdd>;\n\nfunction getErrorId(rootId: string, itemId: string, field: Field) {\n  return `${rootId}-${itemId}-${field}-error`;\n}\n\nfunction removeQuotes(string: string, shouldStrip: boolean): string {\n  if (!shouldStrip) return string;\n\n  const trimmed = string.trim();\n  if (\n    (trimmed.startsWith('\"') && trimmed.endsWith('\"')) ||\n    (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\"))\n  ) {\n    return trimmed.slice(1, -1);\n  }\n  return trimmed;\n}\n\ninterface Store {\n  subscribe: (callback: () => void) => () => void;\n  getState: () => KeyValueState;\n  setState: <K extends keyof KeyValueState>(\n    key: K,\n    value: KeyValueState[K],\n  ) => void;\n  notify: () => void;\n}\n\nfunction useStore<T>(\n  selector: (state: KeyValueState) => T,\n  ogStore?: Store | null,\n): T {\n  const contextStore = React.useContext(StoreContext);\n\n  const store = ogStore ?? contextStore;\n\n  if (!store) {\n    throw new Error(`\\`useStore\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n\n  const getSnapshot = React.useCallback(\n    () => selector(store.getState()),\n    [store, selector],\n  );\n\n  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface ItemData {\n  id: string;\n  key: string;\n  value: string;\n}\n\ninterface KeyValueState {\n  value: ItemData[];\n  focusedId: string | null;\n  errors: Record<string, { key?: string; value?: string }>;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n  const context = React.useContext(StoreContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface KeyValueContextValue {\n  onPaste?: (event: ClipboardEvent, items: ItemData[]) => void;\n  onAdd?: (value: ItemData) => void;\n  onRemove?: (value: ItemData) => void;\n  onKeyValidate?: (key: string, value: ItemData[]) => string | undefined;\n  onValueValidate?: (\n    value: string,\n    key: string,\n    items: ItemData[],\n  ) => string | undefined;\n  rootId: string;\n  maxItems?: number;\n  minItems: number;\n  keyPlaceholder: string;\n  valuePlaceholder: string;\n  allowDuplicateKeys: boolean;\n  enablePaste: boolean;\n  trim: boolean;\n  stripQuotes: boolean;\n  disabled: boolean;\n  readOnly: boolean;\n  required: boolean;\n}\n\nconst KeyValueContext = React.createContext<KeyValueContextValue | null>(null);\n\nfunction useKeyValueContext(consumerName: string) {\n  const context = React.useContext(KeyValueContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface KeyValueProps extends Omit<DivProps, \"onPaste\" | \"defaultValue\"> {\n  id?: string;\n  defaultValue?: ItemData[];\n  value?: ItemData[];\n  onValueChange?: (value: ItemData[]) => void;\n  maxItems?: number;\n  minItems?: number;\n  keyPlaceholder?: string;\n  valuePlaceholder?: string;\n  name?: string;\n  allowDuplicateKeys?: boolean;\n  enablePaste?: boolean;\n  trim?: boolean;\n  stripQuotes?: boolean;\n  disabled?: boolean;\n  readOnly?: boolean;\n  required?: boolean;\n  onPaste?: (event: ClipboardEvent, items: ItemData[]) => void;\n  onAdd?: (value: ItemData) => void;\n  onRemove?: (value: ItemData) => void;\n  onKeyValidate?: (key: string, value: ItemData[]) => string | undefined;\n  onValueValidate?: (\n    value: string,\n    key: string,\n    items: ItemData[],\n  ) => string | undefined;\n}\n\nfunction KeyValue(props: KeyValueProps) {\n  const {\n    value: valueProp,\n    defaultValue,\n    onValueChange,\n    onPaste,\n    onAdd,\n    onRemove,\n    onKeyValidate,\n    onValueValidate,\n    maxItems,\n    minItems = 0,\n    keyPlaceholder = \"Key\",\n    valuePlaceholder = \"Value\",\n    allowDuplicateKeys = false,\n    asChild,\n    enablePaste = true,\n    trim = true,\n    stripQuotes = true,\n    disabled = false,\n    readOnly = false,\n    required = false,\n    className,\n    id,\n    name,\n    ref,\n    ...rootProps\n  } = props;\n\n  const instanceId = React.useId();\n  const rootId = id ?? instanceId;\n\n  const [formTrigger, setFormTrigger] = React.useState<RootElement | null>(\n    null,\n  );\n  const composedRef = useComposedRefs(ref, (node) => setFormTrigger(node));\n  const isFormControl = formTrigger ? !!formTrigger.closest(\"form\") : true;\n\n  const listenersRef = useLazyRef(() => new Set<() => void>());\n  const stateRef = useLazyRef<KeyValueState>(() => ({\n    value: valueProp ??\n      defaultValue ?? [{ id: crypto.randomUUID(), key: \"\", value: \"\" }],\n    focusedId: null,\n    errors: {},\n  }));\n  const propsRef = useAsRef({ onValueChange });\n\n  const store = React.useMemo<Store>(() => {\n    return {\n      subscribe: (cb) => {\n        listenersRef.current.add(cb);\n        return () => listenersRef.current.delete(cb);\n      },\n      getState: () => stateRef.current,\n      setState: (key, val) => {\n        if (Object.is(stateRef.current[key], val)) return;\n\n        if (key === \"value\" && Array.isArray(val)) {\n          stateRef.current.value = val as ItemData[];\n          propsRef.current.onValueChange?.(val as ItemData[]);\n        } else {\n          stateRef.current[key] = val;\n        }\n\n        store.notify();\n      },\n      notify: () => {\n        for (const cb of listenersRef.current) {\n          cb();\n        }\n      },\n    };\n  }, [listenersRef, stateRef, propsRef]);\n\n  const value = useStore((state) => state.value, store);\n  const errors = useStore((state) => state.errors, store);\n  const isInvalid = Object.keys(errors).length > 0;\n\n  useIsomorphicLayoutEffect(() => {\n    if (valueProp !== undefined) {\n      store.setState(\"value\", valueProp);\n    }\n  }, [valueProp]);\n\n  const contextValue = React.useMemo<KeyValueContextValue>(\n    () => ({\n      onPaste,\n      onAdd,\n      onRemove,\n      onKeyValidate,\n      onValueValidate,\n      rootId,\n      maxItems,\n      minItems,\n      keyPlaceholder,\n      valuePlaceholder,\n      allowDuplicateKeys,\n      enablePaste,\n      trim,\n      stripQuotes,\n      disabled,\n      readOnly,\n      required,\n    }),\n    [\n      onPaste,\n      onAdd,\n      onRemove,\n      onKeyValidate,\n      onValueValidate,\n      rootId,\n      disabled,\n      readOnly,\n      required,\n      maxItems,\n      minItems,\n      keyPlaceholder,\n      valuePlaceholder,\n      allowDuplicateKeys,\n      enablePaste,\n      trim,\n      stripQuotes,\n    ],\n  );\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <StoreContext.Provider value={store}>\n      <KeyValueContext.Provider value={contextValue}>\n        <RootPrimitive\n          id={id}\n          data-slot=\"key-value\"\n          data-disabled={disabled ? \"\" : undefined}\n          data-invalid={isInvalid ? \"\" : undefined}\n          data-readonly={readOnly ? \"\" : undefined}\n          {...rootProps}\n          ref={composedRef}\n          className={cn(\"flex flex-col gap-2\", className)}\n        />\n        {isFormControl && (\n          <VisuallyHiddenInput\n            type=\"hidden\"\n            control={formTrigger}\n            name={name}\n            value={value}\n            disabled={disabled}\n            readOnly={readOnly}\n            required={required}\n          />\n        )}\n      </KeyValueContext.Provider>\n    </StoreContext.Provider>\n  );\n}\n\ninterface KeyValueListProps extends DivProps {\n  orientation?: Orientation;\n}\n\nfunction KeyValueList(props: KeyValueListProps) {\n  const { orientation = \"vertical\", asChild, className, ...listProps } = props;\n\n  const value = useStore((state) => state.value);\n\n  const ListPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ListPrimitive\n      role=\"list\"\n      aria-orientation={orientation}\n      data-slot=\"key-value-list\"\n      data-orientation={orientation}\n      {...listProps}\n      className={cn(\n        \"flex\",\n        orientation === \"vertical\" ? \"flex-col gap-2\" : \"flex-row gap-2\",\n        className,\n      )}\n    >\n      {value.map((item) => {\n        const children = React.Children.toArray(props.children);\n\n        return (\n          <KeyValueItemContext.Provider key={item.id} value={item}>\n            {children}\n          </KeyValueItemContext.Provider>\n        );\n      })}\n    </ListPrimitive>\n  );\n}\n\nconst KeyValueItemContext = React.createContext<ItemData | null>(null);\n\nfunction useKeyValueItemContext(consumerName: string) {\n  const context = React.useContext(KeyValueItemContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${LIST_NAME}\\``);\n  }\n  return context;\n}\n\ninterface KeyValueItemProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\nfunction KeyValueItem(props: KeyValueItemProps) {\n  const { asChild, className, ...itemProps } = props;\n  const itemData = useKeyValueItemContext(ITEM_NAME);\n\n  const focusedId = useStore((state) => state.focusedId);\n\n  const ItemPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ItemPrimitive\n      role=\"listitem\"\n      data-slot=\"key-value-item\"\n      data-highlighted={focusedId === itemData.id ? \"\" : undefined}\n      {...itemProps}\n      className={cn(\"flex items-start gap-2\", className)}\n    />\n  );\n}\n\ninterface KeyValueKeyInputProps extends React.ComponentProps<\"input\"> {\n  asChild?: boolean;\n}\n\nfunction KeyValueKeyInput(props: KeyValueKeyInputProps) {\n  const {\n    onChange: onChangeProp,\n    onPaste: onPasteProp,\n    asChild,\n    disabled,\n    readOnly,\n    required,\n    ...keyInputProps\n  } = props;\n\n  const context = useKeyValueContext(KEY_INPUT_NAME);\n  const itemData = useKeyValueItemContext(KEY_INPUT_NAME);\n  const store = useStoreContext(KEY_INPUT_NAME);\n  const errors = useStore((state) => state.errors);\n\n  const propsRef = useAsRef({\n    onChange: onChangeProp,\n    onPaste: onPasteProp,\n  });\n\n  const isDisabled = disabled || context.disabled;\n  const isReadOnly = readOnly || context.readOnly;\n  const isRequired = required || context.required;\n  const isInvalid = errors[itemData.id]?.key !== undefined;\n\n  const onChange = React.useCallback(\n    (event: React.ChangeEvent<KeyInputElement>) => {\n      const state = store.getState();\n      const newValue = state.value.map((item) => {\n        if (item.id !== itemData.id) return item;\n        const updated = { ...item, key: event.target.value };\n        if (context.trim) updated.key = updated.key.trim();\n        return updated;\n      });\n\n      store.setState(\"value\", newValue);\n\n      const updatedItemData = newValue.find((item) => item.id === itemData.id);\n      if (updatedItemData) {\n        const errors: { key?: string; value?: string } = {};\n\n        if (context.onKeyValidate) {\n          const keyError = context.onKeyValidate(updatedItemData.key, newValue);\n          if (keyError) errors.key = keyError;\n        }\n\n        if (!context.allowDuplicateKeys) {\n          const duplicateKey = newValue.find(\n            (item) =>\n              item.id !== updatedItemData.id &&\n              item.key === updatedItemData.key &&\n              updatedItemData.key !== \"\",\n          );\n          if (duplicateKey) {\n            errors.key = \"Duplicate key\";\n          }\n        }\n\n        if (context.onValueValidate) {\n          const valueError = context.onValueValidate(\n            updatedItemData.value,\n            updatedItemData.key,\n            newValue,\n          );\n          if (valueError) errors.value = valueError;\n        }\n\n        const newErrorsState = { ...state.errors };\n        if (Object.keys(errors).length > 0) {\n          newErrorsState[itemData.id] = errors;\n        } else {\n          delete newErrorsState[itemData.id];\n        }\n        store.setState(\"errors\", newErrorsState);\n      }\n\n      propsRef.current.onChange?.(event);\n    },\n    [store, itemData.id, context, propsRef],\n  );\n\n  const onPaste = React.useCallback(\n    (event: React.ClipboardEvent<KeyInputElement>) => {\n      if (!context.enablePaste) return;\n\n      propsRef.current.onPaste?.(event);\n      if (event.defaultPrevented) return;\n\n      const content = event.clipboardData.getData(\"text\");\n      const lines = content.split(/\\r?\\n/).filter((line) => line.trim());\n\n      if (lines.length > 1) {\n        event.preventDefault();\n\n        const parsed: ItemData[] = [];\n\n        for (const line of lines) {\n          let key = \"\";\n          let value = \"\";\n\n          if (line.includes(\"=\")) {\n            const parts = line.split(\"=\");\n            key = parts[0]?.trim() ?? \"\";\n            value = removeQuotes(\n              parts.slice(1).join(\"=\").trim(),\n              context.stripQuotes,\n            );\n          } else if (line.includes(\":\")) {\n            const parts = line.split(\":\");\n            key = parts[0]?.trim() ?? \"\";\n            value = removeQuotes(\n              parts.slice(1).join(\":\").trim(),\n              context.stripQuotes,\n            );\n          } else if (/\\s{2,}|\\t/.test(line)) {\n            const parts = line.split(/\\s{2,}|\\t/);\n            key = parts[0]?.trim() ?? \"\";\n            value = removeQuotes(\n              parts.slice(1).join(\" \").trim(),\n              context.stripQuotes,\n            );\n          }\n\n          if (key) {\n            parsed.push({ id: crypto.randomUUID(), key, value });\n          }\n        }\n\n        if (parsed.length > 0) {\n          const state = store.getState();\n          const currentIndex = state.value.findIndex(\n            (item) => item.id === itemData.id,\n          );\n\n          let newValue: ItemData[];\n          if (itemData.key === \"\" && itemData.value === \"\") {\n            newValue = [\n              ...state.value.slice(0, currentIndex),\n              ...parsed,\n              ...state.value.slice(currentIndex + 1),\n            ];\n          } else {\n            newValue = [\n              ...state.value.slice(0, currentIndex + 1),\n              ...parsed,\n              ...state.value.slice(currentIndex + 1),\n            ];\n          }\n\n          if (context.maxItems !== undefined) {\n            newValue = newValue.slice(0, context.maxItems);\n          }\n\n          store.setState(\"value\", newValue);\n\n          if (context.onPaste) {\n            context.onPaste(\n              event.nativeEvent as unknown as ClipboardEvent,\n              parsed,\n            );\n          }\n        }\n      }\n    },\n    [context, store, itemData, propsRef],\n  );\n\n  const KeyInputPrimitive = asChild ? SlotPrimitive.Slot : Input;\n\n  return (\n    <KeyInputPrimitive\n      aria-invalid={isInvalid}\n      aria-describedby={\n        isInvalid ? getErrorId(context.rootId, itemData.id, \"key\") : undefined\n      }\n      data-slot=\"key-value-key-input\"\n      autoCapitalize=\"off\"\n      autoComplete=\"off\"\n      autoCorrect=\"off\"\n      spellCheck=\"false\"\n      disabled={isDisabled}\n      readOnly={isReadOnly}\n      required={isRequired}\n      placeholder={context.keyPlaceholder}\n      {...keyInputProps}\n      value={itemData.key}\n      onChange={onChange}\n      onPaste={onPaste}\n    />\n  );\n}\n\ninterface KeyValueValueInputProps extends Omit<\n  React.ComponentProps<\"textarea\">,\n  \"rows\"\n> {\n  maxRows?: number;\n  asChild?: boolean;\n}\n\nfunction KeyValueValueInput(props: KeyValueValueInputProps) {\n  const {\n    onChange: onChangeProp,\n    asChild,\n    disabled,\n    readOnly,\n    required,\n    className,\n    maxRows,\n    style,\n    ...valueInputProps\n  } = props;\n\n  const context = useKeyValueContext(VALUE_INPUT_NAME);\n  const itemData = useKeyValueItemContext(VALUE_INPUT_NAME);\n  const store = useStoreContext(VALUE_INPUT_NAME);\n  const errors = useStore((state) => state.errors);\n\n  const propsRef = useAsRef({\n    onChange: onChangeProp,\n  });\n\n  const isDisabled = disabled || context.disabled;\n  const isReadOnly = readOnly || context.readOnly;\n  const isRequired = required || context.required;\n  const isInvalid = errors[itemData.id]?.value !== undefined;\n  const maxHeight = maxRows ? `calc(${maxRows} * 1.5em + 1rem)` : undefined;\n\n  const onChange = React.useCallback(\n    (event: React.ChangeEvent<HTMLTextAreaElement>) => {\n      propsRef.current.onChange?.(event);\n\n      const state = store.getState();\n      const newValue = state.value.map((item) => {\n        if (item.id !== itemData.id) return item;\n        const updated = { ...item, value: event.target.value };\n        if (context.trim) updated.value = updated.value.trim();\n        return updated;\n      });\n\n      store.setState(\"value\", newValue);\n\n      const updatedItemData = newValue.find((item) => item.id === itemData.id);\n      if (updatedItemData) {\n        const errors: { key?: string; value?: string } = {};\n\n        if (context.onKeyValidate) {\n          const keyError = context.onKeyValidate(updatedItemData.key, newValue);\n          if (keyError) errors.key = keyError;\n        }\n\n        if (!context.allowDuplicateKeys) {\n          const duplicateKey = newValue.find(\n            (item) =>\n              item.id !== updatedItemData.id &&\n              item.key === updatedItemData.key &&\n              updatedItemData.key !== \"\",\n          );\n          if (duplicateKey) {\n            errors.key = \"Duplicate key\";\n          }\n        }\n\n        if (context.onValueValidate) {\n          const valueError = context.onValueValidate(\n            updatedItemData.value,\n            updatedItemData.key,\n            newValue,\n          );\n          if (valueError) errors.value = valueError;\n        }\n\n        const newErrorsState = { ...state.errors };\n        if (Object.keys(errors).length > 0) {\n          newErrorsState[itemData.id] = errors;\n        } else {\n          delete newErrorsState[itemData.id];\n        }\n        store.setState(\"errors\", newErrorsState);\n      }\n    },\n    [store, itemData.id, context, propsRef],\n  );\n\n  const ValueInputPrimitive = asChild ? SlotPrimitive.Slot : Textarea;\n\n  return (\n    <ValueInputPrimitive\n      aria-invalid={isInvalid}\n      aria-describedby={\n        isInvalid ? getErrorId(context.rootId, itemData.id, \"value\") : undefined\n      }\n      data-slot=\"key-value-value-input\"\n      autoCapitalize=\"off\"\n      autoComplete=\"off\"\n      autoCorrect=\"off\"\n      spellCheck=\"false\"\n      disabled={isDisabled}\n      readOnly={isReadOnly}\n      required={isRequired}\n      placeholder={context.valuePlaceholder}\n      {...valueInputProps}\n      className={cn(\n        \"field-sizing-content min-h-9 resize-none\",\n        maxRows && \"overflow-y-auto\",\n        className,\n      )}\n      style={{\n        ...style,\n        ...(maxHeight && { maxHeight }),\n      }}\n      value={itemData.value}\n      onChange={onChange}\n    />\n  );\n}\n\ninterface KeyValueRemoveProps extends React.ComponentProps<typeof Button> {}\n\nfunction KeyValueRemove(props: KeyValueRemoveProps) {\n  const { onClick: onClickProp, children, ...removeProps } = props;\n\n  const context = useKeyValueContext(REMOVE_NAME);\n  const itemData = useKeyValueItemContext(REMOVE_NAME);\n  const store = useStoreContext(REMOVE_NAME);\n\n  const propsRef = useAsRef({\n    onClick: onClickProp,\n  });\n  const value = useStore((state) => state.value);\n  const isDisabled = context.disabled || value.length <= context.minItems;\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<RemoveElement>) => {\n      propsRef.current.onClick?.(event);\n\n      const state = store.getState();\n      if (state.value.length <= context.minItems) return;\n\n      const itemToRemove = state.value.find((item) => item.id === itemData.id);\n      if (!itemToRemove) return;\n\n      const newValue = state.value.filter((item) => item.id !== itemData.id);\n      const newErrors = { ...state.errors };\n      delete newErrors[itemData.id];\n\n      store.setState(\"value\", newValue);\n      store.setState(\"errors\", newErrors);\n\n      context.onRemove?.(itemToRemove);\n    },\n    [store, context, itemData.id, propsRef],\n  );\n\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"key-value-remove\"\n      variant=\"outline\"\n      size=\"icon\"\n      disabled={isDisabled}\n      {...removeProps}\n      onClick={onClick}\n    >\n      {children ?? <XIcon />}\n    </Button>\n  );\n}\n\nfunction KeyValueAdd(props: React.ComponentProps<typeof Button>) {\n  const { onClick: onClickProp, children, ...addProps } = props;\n\n  const context = useKeyValueContext(ADD_NAME);\n  const store = useStoreContext(ADD_NAME);\n\n  const propsRef = useAsRef({\n    onClick: onClickProp,\n  });\n  const value = useStore((state) => state.value);\n  const isDisabled =\n    context.disabled ||\n    (context.maxItems !== undefined && value.length >= context.maxItems);\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<AddElement>) => {\n      propsRef.current.onClick?.(event);\n\n      const state = store.getState();\n      if (\n        context.maxItems !== undefined &&\n        state.value.length >= context.maxItems\n      ) {\n        return;\n      }\n\n      const newItem: ItemData = {\n        id: crypto.randomUUID(),\n        key: \"\",\n        value: \"\",\n      };\n\n      const newValue = [...state.value, newItem];\n      store.setState(\"value\", newValue);\n      store.setState(\"focusedId\", newItem.id);\n\n      context.onAdd?.(newItem);\n    },\n    [store, context, propsRef],\n  );\n\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"key-value-add\"\n      variant=\"outline\"\n      disabled={isDisabled}\n      {...addProps}\n      onClick={onClick}\n    >\n      {children ?? (\n        <>\n          <PlusIcon />\n          Add\n        </>\n      )}\n    </Button>\n  );\n}\n\ninterface KeyValueErrorProps extends DivProps {\n  field: Field;\n}\n\nfunction KeyValueError(props: KeyValueErrorProps) {\n  const { field, asChild, className, ...errorProps } = props;\n\n  const context = useKeyValueContext(ERROR_NAME);\n  const itemData = useKeyValueItemContext(ERROR_NAME);\n\n  const errors = useStore((state) => state.errors);\n  const error = errors[itemData.id]?.[field];\n\n  if (!error) return null;\n\n  const ErrorPrimitive = asChild ? SlotPrimitive.Slot : \"span\";\n\n  return (\n    <ErrorPrimitive\n      id={getErrorId(context.rootId, itemData.id, field)}\n      role=\"alert\"\n      {...errorProps}\n      className={cn(\"text-sm font-medium text-destructive\", className)}\n    >\n      {error}\n    </ErrorPrimitive>\n  );\n}\n\nexport {\n  type ItemData as KeyValueItemData,\n  KeyValue,\n  KeyValueAdd,\n  KeyValueError,\n  KeyValueItem,\n  KeyValueKeyInput,\n  KeyValueList,\n  type KeyValueProps,\n  KeyValueRemove,\n  KeyValueValueInput,\n  useStore as useKeyValueStore,\n};\n",
      "target": ""
    },
    {
      "path": "components/visually-hidden-input.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\ntype InputValue = string[] | string;\n\ninterface VisuallyHiddenInputProps<T = InputValue> extends Omit<\n  React.InputHTMLAttributes<HTMLInputElement>,\n  \"value\" | \"checked\" | \"onReset\"\n> {\n  value?: T;\n  checked?: boolean;\n  control: HTMLElement | null;\n  bubbles?: boolean;\n}\n\nfunction VisuallyHiddenInput<T = InputValue>(\n  props: VisuallyHiddenInputProps<T>,\n) {\n  const {\n    control,\n    value,\n    checked,\n    bubbles = true,\n    type = \"hidden\",\n    style,\n    ...inputProps\n  } = props;\n\n  const isCheckInput = React.useMemo(\n    () => type === \"checkbox\" || type === \"radio\" || type === \"switch\",\n    [type],\n  );\n  const inputRef = React.useRef<HTMLInputElement>(null);\n\n  const prevValueRef = React.useRef<{\n    value: T | boolean | undefined;\n    previous: T | boolean | undefined;\n  }>({\n    value: isCheckInput ? checked : value,\n    previous: isCheckInput ? checked : value,\n  });\n\n  const prevValue = React.useMemo(() => {\n    const currentValue = isCheckInput ? checked : value;\n    if (prevValueRef.current.value !== currentValue) {\n      prevValueRef.current.previous = prevValueRef.current.value;\n      prevValueRef.current.value = currentValue;\n    }\n    return prevValueRef.current.previous;\n  }, [isCheckInput, value, checked]);\n\n  const [controlSize, setControlSize] = React.useState<{\n    width?: number;\n    height?: number;\n  }>({});\n\n  React.useLayoutEffect(() => {\n    if (!control) {\n      setControlSize({});\n      return;\n    }\n\n    setControlSize({\n      width: control.offsetWidth,\n      height: control.offsetHeight,\n    });\n\n    if (typeof window === \"undefined\") return;\n\n    const resizeObserver = new ResizeObserver((entries) => {\n      if (!Array.isArray(entries) || !entries.length) return;\n\n      const entry = entries[0];\n      if (!entry) return;\n\n      let width: number;\n      let height: number;\n\n      if (\"borderBoxSize\" in entry) {\n        const borderSizeEntry = entry.borderBoxSize;\n        const borderSize = Array.isArray(borderSizeEntry)\n          ? borderSizeEntry[0]\n          : borderSizeEntry;\n        width = borderSize.inlineSize;\n        height = borderSize.blockSize;\n      } else {\n        width = control.offsetWidth;\n        height = control.offsetHeight;\n      }\n\n      setControlSize({ width, height });\n    });\n\n    resizeObserver.observe(control, { box: \"border-box\" });\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, [control]);\n\n  React.useEffect(() => {\n    const input = inputRef.current;\n    if (!input) return;\n\n    const inputProto = window.HTMLInputElement.prototype;\n    const propertyKey = isCheckInput ? \"checked\" : \"value\";\n    const eventType = isCheckInput ? \"click\" : \"input\";\n    const currentValue = isCheckInput ? checked : value;\n\n    const serializedCurrentValue = isCheckInput\n      ? checked\n      : typeof value === \"object\" && value !== null\n        ? JSON.stringify(value)\n        : value;\n\n    const descriptor = Object.getOwnPropertyDescriptor(inputProto, propertyKey);\n\n    const setter = descriptor?.set;\n\n    if (prevValue !== currentValue && setter) {\n      const event = new Event(eventType, { bubbles });\n      setter.call(input, serializedCurrentValue);\n      input.dispatchEvent(event);\n    }\n  }, [prevValue, value, checked, bubbles, isCheckInput]);\n\n  const composedStyle = React.useMemo<React.CSSProperties>(() => {\n    return {\n      ...style,\n      ...(controlSize.width !== undefined && controlSize.height !== undefined\n        ? controlSize\n        : {}),\n      border: 0,\n      clip: \"rect(0 0 0 0)\",\n      clipPath: \"inset(50%)\",\n      height: \"1px\",\n      margin: \"-1px\",\n      overflow: \"hidden\",\n      padding: 0,\n      position: \"absolute\",\n      whiteSpace: \"nowrap\",\n      width: \"1px\",\n    };\n  }, [style, controlSize]);\n\n  return (\n    <input\n      type={type}\n      {...inputProps}\n      ref={inputRef}\n      aria-hidden={isCheckInput}\n      tabIndex={-1}\n      defaultChecked={isCheckInput ? checked : undefined}\n      style={composedStyle}\n    />\n  );\n}\n\nexport { VisuallyHiddenInput };\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}