diff --git a/packages/components/src/input/backdrop.tsx b/packages/components/src/input/backdrop.tsx
new file mode 100644
index 0000000000000..6b728306ced6e
--- /dev/null
+++ b/packages/components/src/input/backdrop.tsx
@@ -0,0 +1,23 @@
+/**
+ * WordPress dependencies
+ */
+import { memo } from '@wordpress/element';
+/**
+ * Internal dependencies
+ */
+import { BackdropUI } from './styles';
+
+function Backdrop( { disabled = false, isFocused = false } ) {
+ return (
+
+ );
+}
+
+const MemoizedBackdrop = memo( Backdrop );
+
+export default MemoizedBackdrop;
diff --git a/packages/components/src/input/component.tsx b/packages/components/src/input/component.tsx
new file mode 100644
index 0000000000000..c16b71e968fd5
--- /dev/null
+++ b/packages/components/src/input/component.tsx
@@ -0,0 +1,268 @@
+/**
+ * External dependencies
+ */
+import { noop } from 'lodash';
+import { useDrag } from 'react-use-gesture';
+// eslint-disable-next-line no-restricted-imports
+import type { Ref, SyntheticEvent, PointerEvent, MouseEvent } from 'react';
+
+/**
+ * WordPress dependencies
+ */
+import { useRef } from '@wordpress/element';
+import { UP, DOWN, ENTER } from '@wordpress/keycodes';
+/**
+ * Internal dependencies
+ */
+import { useDragCursor } from './utils';
+import { Input as InputView, Container, Prefix, Suffix } from './styles';
+import Backdrop from './backdrop';
+import { useInputControlStateReducer } from './reducer/reducer';
+import { isValueEmpty } from '../utils/values';
+import { useUpdateEffect } from '../utils';
+import {
+ contextConnect,
+ useContextSystem,
+ PolymorphicComponentProps,
+} from '../ui/context';
+import type { Props } from './types';
+
+function Input(
+ props: PolymorphicComponentProps< Props, 'input' >,
+ forwardedRef: Ref< any >
+) {
+ const {
+ disabled = false,
+ dragDirection = 'n',
+ dragThreshold = 10,
+ id,
+ isDragEnabled = false,
+ isFocused,
+ isPressEnterToChange = false,
+ onBlur = noop,
+ onChange = noop,
+ onDrag = noop,
+ onDragEnd = noop,
+ onDragStart = noop,
+ onFocus = noop,
+ onKeyDown = noop,
+ onValidate = noop,
+ size = 'default',
+ setIsFocused,
+ stateReducer = ( state ) => state,
+ value: valueProp,
+ type,
+ hideLabelFromVision = false,
+ __unstableInputWidth,
+ labelPosition,
+ prefix,
+ suffix,
+ ...otherProps
+ } = useContextSystem( props, 'Input' );
+
+ const {
+ // State
+ state,
+ // Actions
+ change,
+ commit,
+ drag,
+ dragEnd,
+ dragStart,
+ invalidate,
+ pressDown,
+ pressEnter,
+ pressUp,
+ reset,
+ update,
+ } = useInputControlStateReducer( stateReducer, {
+ isDragEnabled,
+ value: valueProp,
+ isPressEnterToChange,
+ } );
+
+ const { _event, value, isDragging, isDirty } = state;
+ const wasDirtyOnBlur = useRef( false );
+
+ const dragCursor = useDragCursor( isDragging, dragDirection );
+
+ /*
+ * Handles synchronization of external and internal value state.
+ * If not focused and did not hold a dirty value[1] on blur
+ * updates the value from the props. Otherwise if not holding
+ * a dirty value[1] propagates the value and event through onChange.
+ * [1] value is only made dirty if isPressEnterToChange is true
+ */
+ useUpdateEffect( () => {
+ if ( valueProp === value ) {
+ return;
+ }
+ if ( ! isFocused && ! wasDirtyOnBlur.current ) {
+ update( valueProp, _event as SyntheticEvent );
+ } else if ( ! isDirty ) {
+ onChange( value, { event: _event } );
+ wasDirtyOnBlur.current = false;
+ }
+ }, [ value, isDirty, isFocused, valueProp ] );
+
+ const handleOnBlur = ( event ) => {
+ onBlur( event );
+ setIsFocused( false );
+
+ /**
+ * If isPressEnterToChange is set, this commits the value to
+ * the onChange callback.
+ */
+ if ( isPressEnterToChange && isDirty ) {
+ wasDirtyOnBlur.current = true;
+ if ( ! isValueEmpty( value ) ) {
+ handleOnCommit( event );
+ } else {
+ reset( valueProp, event );
+ }
+ }
+ };
+
+ const handleOnFocus = ( event ) => {
+ onFocus( event );
+ setIsFocused( true );
+ };
+
+ const handleOnChange = ( event ) => {
+ const nextValue = event.target.value;
+ change( nextValue, event );
+ };
+
+ const handleOnCommit = ( event ) => {
+ const nextValue = event.target.value;
+
+ try {
+ onValidate( nextValue );
+ commit( nextValue, event );
+ } catch ( err ) {
+ invalidate( err, event );
+ }
+ };
+
+ const handleOnKeyDown = ( event ) => {
+ const { keyCode } = event;
+ onKeyDown( event );
+
+ switch ( keyCode ) {
+ case UP:
+ pressUp( event );
+ break;
+
+ case DOWN:
+ pressDown( event );
+ break;
+
+ case ENTER:
+ pressEnter( event );
+
+ if ( isPressEnterToChange ) {
+ event.preventDefault();
+ handleOnCommit( event );
+ }
+ break;
+ }
+ };
+
+ const dragGestureProps = useDrag< PointerEvent< HTMLElement > >(
+ ( dragProps ) => {
+ const { distance, dragging, event } = dragProps;
+ // The event is persisted to prevent errors in components using this
+ // to check if a modifier key was held while dragging.
+ ( event as SyntheticEvent ).persist();
+
+ if ( ! distance ) return;
+ event.stopPropagation();
+
+ /**
+ * Quick return if no longer dragging.
+ * This prevents unnecessary value calculations.
+ */
+ if ( ! dragging ) {
+ onDragEnd( dragProps );
+ dragEnd( dragProps );
+ return;
+ }
+
+ onDrag( dragProps );
+ drag( dragProps );
+
+ if ( ! isDragging ) {
+ onDragStart( dragProps );
+ dragStart( dragProps );
+ }
+ },
+ {
+ threshold: dragThreshold,
+ enabled: isDragEnabled,
+ }
+ );
+
+ const dragProps = isDragEnabled ? dragGestureProps() : {};
+ /*
+ * Works around the odd UA (e.g. Firefox) that does not focus inputs of
+ * type=number when their spinner arrows are pressed.
+ */
+ let handleOnMouseDown:
+ | undefined
+ | ( ( event: MouseEvent< HTMLInputElement > ) => void );
+ if ( type === 'number' ) {
+ handleOnMouseDown = ( event ) => {
+ props.onMouseDown?.( event );
+ if (
+ event.target !==
+ ( event.target as HTMLInputElement ).ownerDocument.activeElement
+ ) {
+ ( event.target as HTMLInputElement ).focus();
+ }
+ };
+ }
+
+ return (
+
+ { prefix && (
+
+ { prefix }
+
+ ) }
+
+ { suffix && (
+
+ { suffix }
+
+ ) }
+
+
+ );
+}
+
+const ConnectedInput = contextConnect( Input, 'Input' );
+
+export default ConnectedInput;
diff --git a/packages/components/src/input/index.ts b/packages/components/src/input/index.ts
new file mode 100644
index 0000000000000..88366bbc50e95
--- /dev/null
+++ b/packages/components/src/input/index.ts
@@ -0,0 +1 @@
+export { default as Input } from './component';
diff --git a/packages/components/src/input/reducer/actions.ts b/packages/components/src/input/reducer/actions.ts
new file mode 100644
index 0000000000000..b5eadc2523166
--- /dev/null
+++ b/packages/components/src/input/reducer/actions.ts
@@ -0,0 +1,61 @@
+/**
+ * External dependencies
+ */
+// eslint-disable-next-line no-restricted-imports
+import type { SyntheticEvent } from 'react';
+
+export const CHANGE = 'CHANGE';
+export const COMMIT = 'COMMIT';
+export const DRAG_END = 'DRAG_END';
+export const DRAG_START = 'DRAG_START';
+export const DRAG = 'DRAG';
+export const INVALIDATE = 'INVALIDATE';
+export const PRESS_DOWN = 'PRESS_DOWN';
+export const PRESS_ENTER = 'PRESS_ENTER';
+export const PRESS_UP = 'PRESS_UP';
+export const RESET = 'RESET';
+export const UPDATE = 'UPDATE';
+
+interface EventPayload {
+ event?: SyntheticEvent;
+}
+
+interface Action< Type, ExtraPayload = {} > {
+ type: Type;
+ payload: EventPayload & ExtraPayload;
+}
+
+interface ValuePayload {
+ value: string;
+}
+
+export type ChangeAction = Action< typeof CHANGE, ValuePayload >;
+export type CommitAction = Action< typeof COMMIT, ValuePayload >;
+export type PressUpAction = Action< typeof PRESS_UP >;
+export type PressDownAction = Action< typeof PRESS_DOWN >;
+export type PressEnterAction = Action< typeof PRESS_ENTER >;
+export type DragStartAction = Action< typeof DRAG_START >;
+export type DragEndAction = Action< typeof DRAG_END >;
+export type DragAction = Action< typeof DRAG >;
+export type ResetAction = Action< typeof RESET, Partial< ValuePayload > >;
+export type UpdateAction = Action< typeof UPDATE, ValuePayload >;
+export type InvalidateAction = Action<
+ typeof INVALIDATE,
+ { error: Error | null }
+>;
+
+export type ChangeEventAction =
+ | ChangeAction
+ | ResetAction
+ | CommitAction
+ | UpdateAction;
+
+export type DragEventAction = DragStartAction | DragEndAction | DragAction;
+
+export type KeyEventAction = PressDownAction | PressUpAction | PressEnterAction;
+
+export type InputAction =
+ | ChangeEventAction
+ | KeyEventAction
+ | DragEventAction
+ | InvalidateAction;
diff --git a/packages/components/src/input/reducer/reducer.ts b/packages/components/src/input/reducer/reducer.ts
new file mode 100644
index 0000000000000..0369d153c9dca
--- /dev/null
+++ b/packages/components/src/input/reducer/reducer.ts
@@ -0,0 +1,245 @@
+/**
+ * External dependencies
+ */
+import { isEmpty } from 'lodash';
+// eslint-disable-next-line no-restricted-imports
+import type { SyntheticEvent } from 'react';
+
+/**
+ * WordPress dependencies
+ */
+import { useReducer } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import {
+ InputState,
+ StateReducer,
+ initialInputControlState,
+ initialStateReducer,
+} from './state';
+import * as actions from './actions';
+
+/**
+ * Prepares initialState for the reducer.
+ *
+ * @param initialState The initial state.
+ * @return Prepared initialState for the reducer
+ */
+function mergeInitialState(
+ initialState: Partial< InputState > = initialInputControlState
+): InputState {
+ const { value } = initialState;
+
+ return {
+ ...initialInputControlState,
+ ...initialState,
+ initialValue: value,
+ };
+}
+
+/**
+ * Composes multiple stateReducers into a single stateReducer, building
+ * the pipeline to control the flow for state and actions.
+ *
+ * @param fns State reducers.
+ * @return The single composed stateReducer.
+ */
+export const composeStateReducers = (
+ ...fns: StateReducer[]
+): StateReducer => {
+ return ( ...args ) => {
+ return fns.reduceRight( ( state, fn ) => {
+ const fnState = fn( ...args );
+ return isEmpty( fnState ) ? state : { ...state, ...fnState };
+ }, {} as InputState );
+ };
+};
+
+/**
+ * Creates a reducer that opens the channel for external state subscription
+ * and modification.
+ *
+ * This technique uses the "stateReducer" design pattern:
+ * https://kentcdodds.com/blog/the-state-reducer-pattern/
+ *
+ * @param composedStateReducers A custom reducer that can subscribe and modify state.
+ * @return The reducer.
+ */
+function inputControlStateReducer(
+ composedStateReducers: StateReducer
+): StateReducer {
+ return ( state, action ) => {
+ const nextState = { ...state };
+
+ switch ( action.type ) {
+ /**
+ * Keyboard events
+ */
+ case actions.PRESS_UP:
+ nextState.isDirty = false;
+ break;
+
+ case actions.PRESS_DOWN:
+ nextState.isDirty = false;
+ break;
+
+ /**
+ * Drag events
+ */
+ case actions.DRAG_START:
+ nextState.isDragging = true;
+ break;
+
+ case actions.DRAG_END:
+ nextState.isDragging = false;
+ break;
+
+ /**
+ * Input events
+ */
+ case actions.CHANGE:
+ nextState.error = null;
+ nextState.value = action.payload.value;
+
+ if ( state.isPressEnterToChange ) {
+ nextState.isDirty = true;
+ }
+
+ break;
+
+ case actions.COMMIT:
+ nextState.value = action.payload.value;
+ nextState.isDirty = false;
+ break;
+
+ case actions.RESET:
+ nextState.error = null;
+ nextState.isDirty = false;
+ nextState.value = action.payload.value || state.initialValue;
+ break;
+
+ case actions.UPDATE:
+ nextState.value = action.payload.value;
+ nextState.isDirty = false;
+ break;
+
+ /**
+ * Validation
+ */
+ case actions.INVALIDATE:
+ nextState.error = action.payload.error;
+ break;
+ }
+
+ if ( action.payload.event ) {
+ nextState._event = action.payload.event;
+ }
+
+ /**
+ * Send the nextState + action to the composedReducers via
+ * this "bridge" mechanism. This allows external stateReducers
+ * to hook into actions, and modify state if needed.
+ */
+ return composedStateReducers( nextState, action );
+ };
+}
+
+/**
+ * A custom hook that connects and external stateReducer with an internal
+ * reducer. This hook manages the internal state of InputControl.
+ * However, by connecting an external stateReducer function, other
+ * components can react to actions as well as modify state before it is
+ * applied.
+ *
+ * This technique uses the "stateReducer" design pattern:
+ * https://kentcdodds.com/blog/the-state-reducer-pattern/
+ *
+ * @param stateReducer An external state reducer.
+ * @param initialState The initial state for the reducer.
+ * @return State, dispatch, and a collection of actions.
+ */
+export function useInputControlStateReducer(
+ stateReducer: StateReducer = initialStateReducer,
+ initialState: Partial< InputState > = initialInputControlState
+) {
+ const [ state, dispatch ] = useReducer< StateReducer >(
+ inputControlStateReducer( stateReducer ),
+ mergeInitialState( initialState )
+ );
+
+ const createChangeEvent = ( type: actions.ChangeEventAction[ 'type' ] ) => (
+ nextValue: actions.ChangeEventAction[ 'payload' ][ 'value' ],
+ event: actions.ChangeEventAction[ 'payload' ][ 'event' ]
+ ) => {
+ /**
+ * Persist allows for the (Synthetic) event to be used outside of
+ * this function call.
+ * https://reactjs.org/docs/events.html#event-pooling
+ */
+ if ( event && event.persist ) {
+ event.persist();
+ }
+
+ dispatch( {
+ type,
+ payload: { value: nextValue, event },
+ } );
+ };
+
+ const createKeyEvent = ( type: actions.KeyEventAction[ 'type' ] ) => (
+ event: actions.KeyEventAction[ 'payload' ][ 'event' ]
+ ) => {
+ /**
+ * Persist allows for the (Synthetic) event to be used outside of
+ * this function call.
+ * https://reactjs.org/docs/events.html#event-pooling
+ */
+ if ( event && event.persist ) {
+ event.persist();
+ }
+
+ dispatch( { type, payload: { event } } );
+ };
+
+ const createDragEvent = ( type: actions.DragEventAction[ 'type' ] ) => (
+ payload: actions.DragEventAction[ 'payload' ]
+ ) => {
+ dispatch( { type, payload } );
+ };
+
+ /**
+ * Actions for the reducer
+ */
+ const change = createChangeEvent( actions.CHANGE );
+ const invalidate = ( error: Error, event: SyntheticEvent ) =>
+ dispatch( { type: actions.INVALIDATE, payload: { error, event } } );
+ const reset = createChangeEvent( actions.RESET );
+ const commit = createChangeEvent( actions.COMMIT );
+ const update = createChangeEvent( actions.UPDATE );
+
+ const dragStart = createDragEvent( actions.DRAG_START );
+ const drag = createDragEvent( actions.DRAG );
+ const dragEnd = createDragEvent( actions.DRAG_END );
+
+ const pressUp = createKeyEvent( actions.PRESS_UP );
+ const pressDown = createKeyEvent( actions.PRESS_DOWN );
+ const pressEnter = createKeyEvent( actions.PRESS_ENTER );
+
+ return {
+ change,
+ commit,
+ dispatch,
+ drag,
+ dragEnd,
+ dragStart,
+ invalidate,
+ pressDown,
+ pressEnter,
+ pressUp,
+ reset,
+ state,
+ update,
+ } as const;
+}
diff --git a/packages/components/src/input/reducer/state.ts b/packages/components/src/input/reducer/state.ts
new file mode 100644
index 0000000000000..6c8dc2e8397e6
--- /dev/null
+++ b/packages/components/src/input/reducer/state.ts
@@ -0,0 +1,36 @@
+/**
+ * External dependencies
+ */
+// eslint-disable-next-line no-restricted-imports
+import type { Reducer } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import type { InputAction } from './actions';
+
+export interface InputState {
+ _event: Event | {};
+ error: Error | null;
+ initialValue: string;
+ isDirty: boolean;
+ isDragEnabled: boolean;
+ isDragging: boolean;
+ isPressEnterToChange: boolean;
+ value: string;
+}
+
+export type StateReducer = Reducer< InputState, InputAction >;
+
+export const initialStateReducer: StateReducer = ( state: InputState ) => state;
+
+export const initialInputControlState: InputState = {
+ _event: {},
+ error: null,
+ initialValue: '',
+ isDirty: false,
+ isDragEnabled: false,
+ isDragging: false,
+ isPressEnterToChange: false,
+ value: '',
+};
diff --git a/packages/components/src/input/stories/index.js b/packages/components/src/input/stories/index.js
new file mode 100644
index 0000000000000..193fedb1729a2
--- /dev/null
+++ b/packages/components/src/input/stories/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import { Input } from '..';
+
+export default {
+ component: Input,
+ title: 'Components (Experimental)/Input',
+};
+
+export const _default = () => {
+ return ;
+};
diff --git a/packages/components/src/input/styles.tsx b/packages/components/src/input/styles.tsx
new file mode 100644
index 0000000000000..bb8bfb59f0702
--- /dev/null
+++ b/packages/components/src/input/styles.tsx
@@ -0,0 +1,234 @@
+/**
+ * External dependencies
+ */
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+// eslint-disable-next-line no-restricted-imports
+import type { CSSProperties } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { Flex } from '../flex';
+import { COLORS, rtl } from '../utils';
+
+type ContainerProps = {
+ disabled: boolean;
+ hideLabel: boolean;
+ __unstableInputWidth?: CSSProperties[ 'width' ];
+ labelPosition: 'side' | 'edge';
+};
+
+const containerDisabledStyles = ( { disabled }: ContainerProps ) => {
+ const backgroundColor = disabled
+ ? COLORS.ui.backgroundDisabled
+ : COLORS.ui.background;
+
+ return css( { backgroundColor } );
+};
+
+// Normalizes the margins from the (components/ui/flex/) container.
+const containerMarginStyles = ( { hideLabel }: ContainerProps ) => {
+ return hideLabel ? css( { margin: '0 !important' } ) : null;
+};
+
+const containerWidthStyles = ( {
+ __unstableInputWidth,
+ labelPosition,
+}: ContainerProps ) => {
+ if ( ! __unstableInputWidth ) return css( { width: '100%' } );
+
+ if ( labelPosition === 'side' ) return '';
+
+ if ( labelPosition === 'edge' ) {
+ return css( {
+ flex: `0 0 ${ __unstableInputWidth }`,
+ } );
+ }
+
+ return css( { width: __unstableInputWidth } );
+};
+
+export const Container = styled.div< ContainerProps >`
+ align-items: center;
+ box-sizing: border-box;
+ border-radius: inherit;
+ display: flex;
+ flex: 1;
+ position: relative;
+
+ ${ containerDisabledStyles }
+ ${ containerMarginStyles }
+ ${ containerWidthStyles }
+`;
+
+type InputProps = Partial< {
+ isDragging: boolean;
+ dragCursor: 'ns-resize' | 'ew-resize';
+ disabled: boolean;
+ inputSize: 'default' | 'small';
+} >;
+
+const disabledStyles = ( { disabled }: InputProps ) => {
+ if ( ! disabled ) return '';
+
+ return css( {
+ color: COLORS.ui.textDisabled,
+ } );
+};
+
+const fontSizeStyles = ( { inputSize: size }: InputProps ) => {
+ const sizes = {
+ default: '13px',
+ small: '11px',
+ };
+
+ const fontSize = sizes[ size ];
+ const fontSizeMobile = '16px';
+
+ if ( ! fontSize ) return '';
+
+ return css`
+ font-size: ${ fontSizeMobile };
+
+ @media ( min-width: 600px ) {
+ font-size: ${ fontSize };
+ }
+ `;
+};
+
+const sizeStyles = ( { inputSize: size }: InputProps ) => {
+ const sizes = {
+ default: {
+ height: 30,
+ lineHeight: 1,
+ minHeight: 30,
+ },
+ small: {
+ height: 24,
+ lineHeight: 1,
+ minHeight: 24,
+ },
+ };
+
+ const style = sizes[ size ] || sizes.default;
+
+ return css( style );
+};
+
+const placeholderStyles = () => {
+ return css`
+ &::-webkit-input-placeholder {
+ line-height: normal;
+ }
+ `;
+};
+
+const dragStyles = ( { isDragging, dragCursor }: InputProps ) => {
+ let defaultArrowStyles;
+ let activeDragCursorStyles;
+
+ if ( isDragging ) {
+ defaultArrowStyles = css`
+ cursor: ${ dragCursor };
+ user-select: none;
+
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none !important;
+ margin: 0 !important;
+ }
+ `;
+ }
+
+ if ( isDragging && dragCursor ) {
+ activeDragCursorStyles = css`
+ &:active {
+ cursor: ${ dragCursor };
+ }
+ `;
+ }
+
+ return css`
+ ${ defaultArrowStyles }
+ ${ activeDragCursorStyles }
+ `;
+};
+
+// TODO: Resolve need to use &&& to increase specificity
+// https://github.com/WordPress/gutenberg/issues/18483
+
+export const Input = styled.input< InputProps >`
+ &&& {
+ background-color: transparent;
+ box-sizing: border-box;
+ border: none;
+ box-shadow: none !important;
+ color: ${ COLORS.black };
+ display: block;
+ margin: 0;
+ outline: none;
+ padding-left: 8px;
+ padding-right: 8px;
+ width: 100%;
+
+ ${ dragStyles }
+ ${ disabledStyles }
+ ${ fontSizeStyles }
+ ${ sizeStyles }
+
+ ${ placeholderStyles }
+ }
+`;
+
+const backdropFocusedStyles = ( { disabled, isFocused } ) => {
+ let borderColor = isFocused ? COLORS.ui.borderFocus : COLORS.ui.border;
+
+ let boxShadow = null;
+
+ if ( isFocused ) {
+ boxShadow = `0 0 0 1px ${ COLORS.ui.borderFocus } inset`;
+ }
+
+ if ( disabled ) {
+ borderColor = COLORS.ui.borderDisabled;
+ }
+
+ return css( {
+ boxShadow,
+ borderColor,
+ borderStyle: 'solid',
+ borderWidth: 1,
+ } );
+};
+
+export const BackdropUI = styled.div< {
+ disabled: boolean;
+ isFocused: boolean;
+} >`
+ &&& {
+ box-sizing: border-box;
+ border-radius: inherit;
+ bottom: 0;
+ left: 0;
+ margin: 0;
+ padding: 0;
+ pointer-events: none;
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ ${ backdropFocusedStyles }
+ ${ rtl( { paddingLeft: 2 } ) }
+ }
+`;
+
+export const Prefix = styled.span`
+ box-sizing: border-box;
+ display: block;
+`;
+
+export const Suffix = styled.span`
+ box-sizing: border-box;
+ display: block;
+`;
diff --git a/packages/components/src/input/types.ts b/packages/components/src/input/types.ts
new file mode 100644
index 0000000000000..1893af7cd2f35
--- /dev/null
+++ b/packages/components/src/input/types.ts
@@ -0,0 +1,30 @@
+/**
+ * External dependencies
+ */
+// eslint-disable-next-line no-restricted-imports
+import type { CSSProperties, ReactNode } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import type { StateReducer } from './reducer/state';
+
+export type DragDirection = 'n' | 's' | 'e' | 'w';
+
+export interface Props {
+ dragDirection?: DragDirection;
+ dragThreshold?: number;
+ isDragEnabled?: boolean;
+ isFocused?: boolean;
+ isPressEnterToChange?: boolean;
+ onValidate?: ( nextValue: string ) => void;
+ setIsFocused?: ( isFocused: boolean ) => void;
+ stateReducer?: StateReducer;
+ value?: string;
+ size?: 'default' | 'small';
+ hideLabelFromVision?: boolean;
+ __unstableInputWidth?: CSSProperties[ 'width' ];
+ prefix?: ReactNode;
+ suffix?: ReactNode;
+ labelPosition?: 'side' | 'edge';
+}
diff --git a/packages/components/src/input/utils.ts b/packages/components/src/input/utils.ts
new file mode 100644
index 0000000000000..c9a2b9c4c7132
--- /dev/null
+++ b/packages/components/src/input/utils.ts
@@ -0,0 +1,60 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import type { DragDirection } from './types';
+
+/**
+ * Gets a CSS cursor value based on a drag direction.
+ *
+ * @param dragDirection The drag direction.
+ * @return The CSS cursor value.
+ */
+export function getDragCursor(
+ dragDirection: DragDirection
+): 'ns-resize' | 'ew-resize' {
+ let dragCursor: 'ns-resize' | 'ew-resize' = 'ns-resize';
+
+ switch ( dragDirection ) {
+ case 'n':
+ case 's':
+ dragCursor = 'ns-resize';
+ break;
+
+ case 'e':
+ case 'w':
+ dragCursor = 'ew-resize';
+ break;
+ }
+
+ return dragCursor;
+}
+
+/**
+ * Custom hook that renders a drag cursor when dragging.
+ *
+ * @param isDragging The dragging state.
+ * @param dragDirection The drag direction.
+ *
+ * @return The CSS cursor value.
+ */
+export function useDragCursor(
+ isDragging: boolean,
+ dragDirection: DragDirection
+): 'ns-resize' | 'ew-resize' {
+ const dragCursor = getDragCursor( dragDirection );
+
+ useEffect( () => {
+ if ( isDragging ) {
+ document.documentElement.style.cursor = dragCursor;
+ } else {
+ document.documentElement.style.cursor = null;
+ }
+ }, [ isDragging ] );
+
+ return dragCursor;
+}
diff --git a/packages/components/src/ui/color-picker b/packages/components/src/ui/color-picker
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/packages/components/src/utils/rtl.js b/packages/components/src/utils/rtl.js
index 9695e5539be97..e5ec6e17d62f0 100644
--- a/packages/components/src/utils/rtl.js
+++ b/packages/components/src/utils/rtl.js
@@ -66,7 +66,7 @@ export const convertLTRToRTL = ( ltrStyles = {} ) => {
* @param {import('react').CSSProperties} ltrStyles Ltr styles. Converts and renders from ltr -> rtl styles, if applicable.
* @param {import('react').CSSProperties} [rtlStyles] Rtl styles. Renders if provided.
*
- * @return {Function} A function to output CSS styles for Emotion's renderer
+ * @return {() => import('@emotion/core').SerializedStyles} A function to output CSS styles for Emotion's renderer
*/
export function rtl( ltrStyles = {}, rtlStyles ) {
return () => {