+ {data.map((item, index) => {
+ const isActive = dragState?.activeIndex === index
+
+ // Clamp translation so the item stays within list bounds.
+ const rawTranslationY = isActive
+ ? dragState.currentY - dragState.startY
+ : 0
+ const translationY = isActive
+ ? Math.max(
+ -index * itemHeight,
+ Math.min(rawTranslationY, (data.length - 1 - index) * itemHeight),
+ )
+ : 0
+
+ // Non-dragged items shift to make room for the dragged item.
+ let offset = 0
+ if (dragState && !isActive) {
+ const orig = dragState.activeIndex
+ if (orig < newPos && index > orig && index <= newPos) {
+ offset = -itemHeight
+ } else if (orig > newPos && index < orig && index >= newPos) {
+ offset = itemHeight
+ }
+ }
+
+ const dragHandle = (
+ ) =>
+ handlePointerDown(e, index)
+ }
+ style={{
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingLeft: 8,
+ paddingRight: 8,
+ paddingTop: 12,
+ paddingBottom: 12,
+ cursor: isActive ? 'grabbing' : 'grab',
+ touchAction: 'none',
+ userSelect: 'none',
+ }}>
+
+
+ )
+
+ return (
+
+ {renderItem(item, dragHandle)}
+
+ )
+ })}
+
+ )
+}
diff --git a/src/components/PostControls/PostMenu/index.tsx b/src/components/PostControls/PostMenu/index.tsx
index 65a948d69b..dbf0744990 100644
--- a/src/components/PostControls/PostMenu/index.tsx
+++ b/src/components/PostControls/PostMenu/index.tsx
@@ -11,7 +11,7 @@ import {useLingui} from '@lingui/react'
import {type Shadow} from '#/state/cache/post-shadow'
import {EventStopper} from '#/view/com/util/EventStopper'
-import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
+import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
import {useMenuControl} from '#/components/Menu'
import * as Menu from '#/components/Menu'
import {PostControlButton, PostControlButtonIcon} from '../PostControlButton'
diff --git a/src/components/dms/ActionsWrapper.web.tsx b/src/components/dms/ActionsWrapper.web.tsx
index 18eb4161cf..6b95e7fbaa 100644
--- a/src/components/dms/ActionsWrapper.web.tsx
+++ b/src/components/dms/ActionsWrapper.web.tsx
@@ -9,7 +9,7 @@ import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
-import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
+import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
import {EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
import {EmojiReactionPicker} from './EmojiReactionPicker'
import {hasReachedReactionLimit} from './util'
diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx
index 713605a7af..6fcca20814 100644
--- a/src/components/dms/ConvoMenu.tsx
+++ b/src/components/dms/ConvoMenu.tsx
@@ -24,7 +24,7 @@ import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft'
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
-import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
+import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
import {
diff --git a/src/components/dms/EmojiReactionPicker.web.tsx b/src/components/dms/EmojiReactionPicker.web.tsx
index 6a96238797..6be85efb4c 100644
--- a/src/components/dms/EmojiReactionPicker.web.tsx
+++ b/src/components/dms/EmojiReactionPicker.web.tsx
@@ -10,7 +10,7 @@ import {useSession} from '#/state/session'
import {type Emoji} from '#/view/com/composer/text-input/web/EmojiPicker'
import {useWebPreloadEmoji} from '#/view/com/composer/text-input/web/useWebPreloadEmoji'
import {atoms as a, flatten, useTheme} from '#/alf'
-import {DotGrid_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid'
+import {DotGrid3x1_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid'
import * as Menu from '#/components/Menu'
import {type TriggerProps} from '#/components/Menu/types'
import {Text} from '#/components/Typography'
diff --git a/src/components/icons/DotGrid.tsx b/src/components/icons/DotGrid.tsx
index c50d7a440f..2a21026664 100644
--- a/src/components/icons/DotGrid.tsx
+++ b/src/components/icons/DotGrid.tsx
@@ -1,5 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
-export const DotGrid_Stroke2_Corner0_Rounded = createSinglePathSVG({
+export const DotGrid3x1_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M2 12a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm16 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm-6-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z',
})
+
+export const DotGrid2x3_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M9 17a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm6 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm-6-7a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm6 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4ZM9 3a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm6 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4Z',
+})
diff --git a/src/screens/Profile/components/ProfileFeedHeader.tsx b/src/screens/Profile/components/ProfileFeedHeader.tsx
index 2201fee749..58f6fc5942 100644
--- a/src/screens/Profile/components/ProfileFeedHeader.tsx
+++ b/src/screens/Profile/components/ProfileFeedHeader.tsx
@@ -30,7 +30,7 @@ import {Divider} from '#/components/Divider'
import {useRichText} from '#/components/hooks/useRichText'
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
-import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
+import {DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
import {
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
Heart2_Stroke2_Corner0_Rounded as Heart,
diff --git a/src/screens/ProfileList/components/MoreOptionsMenu.tsx b/src/screens/ProfileList/components/MoreOptionsMenu.tsx
index d414c53514..d15664618c 100644
--- a/src/screens/ProfileList/components/MoreOptionsMenu.tsx
+++ b/src/screens/ProfileList/components/MoreOptionsMenu.tsx
@@ -20,7 +20,7 @@ import {useDialogControl} from '#/components/Dialog'
import {CreateOrEditListDialog} from '#/components/dialogs/lists/CreateOrEditListDialog'
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox'
import {ChainLink_Stroke2_Corner0_Rounded as ChainLink} from '#/components/icons/ChainLink'
-import {DotGrid_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid'
+import {DotGrid3x1_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid'
import {PencilLine_Stroke2_Corner0_Rounded as PencilLineIcon} from '#/components/icons/Pencil'
import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheckIcon} from '#/components/icons/Person'
import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
diff --git a/src/screens/SavedFeeds.tsx b/src/screens/SavedFeeds.tsx
index df57297241..4b8fcaddbc 100644
--- a/src/screens/SavedFeeds.tsx
+++ b/src/screens/SavedFeeds.tsx
@@ -1,6 +1,7 @@
import {useCallback, useState} from 'react'
import {View} from 'react-native'
-import Animated, {LinearTransition} from 'react-native-reanimated'
+import type Animated from 'react-native-reanimated'
+import {useAnimatedRef, useScrollViewOffset} from 'react-native-reanimated'
import {type AppBskyActorDefs} from '@atproto/api'
import {TID} from '@atproto/common-web'
import {msg} from '@lingui/core/macro'
@@ -16,6 +17,7 @@ import {
type NavigationProp,
} from '#/lib/routes/types'
import {logger} from '#/logger'
+import {useA11y} from '#/state/a11y'
import {
useOverwriteSavedFeedsMutation,
usePreferencesQuery,
@@ -29,6 +31,7 @@ import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {SortableList} from '#/components/DraggableList'
import {
ArrowBottom_Stroke2_Corner0_Rounded as ArrowDownIcon,
ArrowTop_Stroke2_Corner0_Rounded as ArrowUpIcon,
@@ -45,9 +48,13 @@ import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps