{
+ // Only surface on keyboard focus. Programmatic focus after a mouse action (e.g. a dropdown
+ // moving focus to its first item on open) doesn't match :focus-visible, so it won't pop the tooltip.
+ const target = e.target as HTMLElement;
+ if (!isExpanded() && target.matches?.(':focus-visible')) show();
+ }}
+ onBlur={hide}
+ onPointerEnter={(e) => !isTouchActive.current && e.pointerType !== 'touch' && !isExpanded() && show()}
+ onPointerLeave={(e) => !isTouchActive.current && e.pointerType !== 'touch' && hide()}
+ onKeyDown={(e) => e.key === 'Escape' && hide()}
+ onTouchStart={handleTouchStart}
+ onTouchEnd={handleTouchEnd}
+ onTouchMove={handleTouchMove}
+ onTouchCancel={handleTouchMove}
+ onClickCapture={handleClickCapture}
+ >
+
+ {content}
+
+ {React.isValidElement(children)
+ ? React.cloneElement(children, { 'aria-describedby': tooltipId } as any)
+ : children}
+
+ );
+};
+
+export default Tooltip;
diff --git a/src/v2/components/ui/Tooltip/index.tsx b/src/v2/components/ui/Tooltip/index.tsx
new file mode 100644
index 000000000..cdc0fab16
--- /dev/null
+++ b/src/v2/components/ui/Tooltip/index.tsx
@@ -0,0 +1 @@
+export { default } from './Tooltip';
diff --git a/src/v2/forms/BoxForm/BoxForm.tsx b/src/v2/forms/BoxForm/BoxForm.tsx
new file mode 100644
index 000000000..899c1398d
--- /dev/null
+++ b/src/v2/forms/BoxForm/BoxForm.tsx
@@ -0,0 +1,170 @@
+import TextInput from '@/v2/components/input/TextInput';
+import Button from '@/v2/components/button/Button';
+import { Controller, useForm } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { BoxType } from '@/types/Scopes';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+import RoomField from '../fields/RoomField';
+import PhaseField from '../fields/PhaseField';
+import RichEditor from '@/v2/components/input/RichEditor';
+import { useDraftStorage } from '@/v2/hooks';
+
+const MAX_CHAR_COUNT = 1000;
+const MAX_NAME_LENGTH = 200;
+
+interface BoxFormProps {
+ defaultValues?: BoxType;
+ /** Returns `true` when the box was persisted, so the form can clear its draft. */
+ onSubmit: (data: any) => Promise