about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Navigation.tsx25
-rw-r--r--src/components/FeedCard.tsx68
-rw-r--r--src/components/ProfileCard.tsx35
-rw-r--r--src/components/ProgressGuide/FollowDialog.tsx15
-rw-r--r--src/components/StarterPack/StarterPackCard.tsx31
-rw-r--r--src/components/icons/Flame.tsx5
-rw-r--r--src/components/icons/Trending.tsx (renamed from src/components/icons/Trending2.tsx)4
-rw-r--r--src/components/icons/common.tsx6
-rw-r--r--src/components/interstitials/Trending.tsx2
-rw-r--r--src/components/interstitials/TrendingVideos.tsx2
-rw-r--r--src/lib/icons.tsx2
-rw-r--r--src/lib/statsig/gates.ts1
-rw-r--r--src/logger/metrics.ts20
-rw-r--r--src/screens/Onboarding/StepFinished.tsx10
-rw-r--r--src/screens/Profile/ProfileSearch.tsx7
-rw-r--r--src/screens/Search/Explore.tsx923
-rw-r--r--src/screens/Search/SearchResults.tsx338
-rw-r--r--src/screens/Search/Shell.tsx535
-rw-r--r--src/screens/Search/components/AutocompleteResults.tsx71
-rw-r--r--src/screens/Search/components/ExploreTrendingTopics.tsx142
-rw-r--r--src/screens/Search/components/ModuleHeader.tsx170
-rw-r--r--src/screens/Search/components/SearchHistory.tsx169
-rw-r--r--src/screens/Search/components/SearchLanguageDropdown.tsx120
-rw-r--r--src/screens/Search/components/StarterPackCard.tsx296
-rw-r--r--src/screens/Search/index.tsx13
-rw-r--r--src/screens/Search/modules/ExploreFeedPreviews.tsx264
-rw-r--r--src/screens/Search/modules/ExploreRecommendations.tsx (renamed from src/screens/Search/components/ExploreRecommendations.tsx)12
-rw-r--r--src/screens/Search/modules/ExploreSuggestedAccounts.tsx228
-rw-r--r--src/screens/Search/modules/ExploreTrendingTopics.tsx278
-rw-r--r--src/screens/Search/modules/ExploreTrendingVideos.tsx (renamed from src/screens/Search/components/ExploreTrendingVideos.tsx)109
-rw-r--r--src/screens/Settings/ContentAndMediaSettings.tsx6
-rw-r--r--src/state/queries/actor-search.ts23
-rw-r--r--src/state/queries/trending/useGetSuggestedFeedsQuery.ts48
-rw-r--r--src/state/queries/trending/useGetTrendsQuery.ts59
-rw-r--r--src/state/queries/useSuggestedStarterPacksQuery.ts38
-rw-r--r--src/view/com/posts/PostFeed.tsx26
-rw-r--r--src/view/com/util/numeric/format.ts2
-rw-r--r--src/view/screens/Search/Explore.tsx641
-rw-r--r--src/view/screens/Search/Search.tsx1165
-rw-r--r--src/view/screens/Search/index.tsx1
-rw-r--r--src/view/shell/desktop/SidebarTrendingTopics.tsx2
41 files changed, 3797 insertions, 2115 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 807fd92e5..420a49d4c 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -1,9 +1,8 @@
 import * as React from 'react'
-import {JSX} from 'react/jsx-runtime'
-import {i18n, MessageDescriptor} from '@lingui/core'
+import {i18n, type MessageDescriptor} from '@lingui/core'
 import {msg} from '@lingui/macro'
 import {
-  BottomTabBarProps,
+  type BottomTabBarProps,
   createBottomTabNavigator,
 } from '@react-navigation/bottom-tabs'
 import {
@@ -20,16 +19,16 @@ import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
 import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
 import {buildStateObject} from '#/lib/routes/helpers'
 import {
-  AllNavigatorParams,
-  BottomTabNavigatorParams,
-  FlatNavigatorParams,
-  HomeTabNavigatorParams,
-  MessagesTabNavigatorParams,
-  MyProfileTabNavigatorParams,
-  NotificationsTabNavigatorParams,
-  SearchTabNavigatorParams,
+  type AllNavigatorParams,
+  type BottomTabNavigatorParams,
+  type FlatNavigatorParams,
+  type HomeTabNavigatorParams,
+  type MessagesTabNavigatorParams,
+  type MyProfileTabNavigatorParams,
+  type NotificationsTabNavigatorParams,
+  type SearchTabNavigatorParams,
 } from '#/lib/routes/types'
-import {RouteParams, State} from '#/lib/routes/types'
+import {type RouteParams, type State} from '#/lib/routes/types'
 import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
 import {bskyTitle} from '#/lib/strings/headings'
 import {logger} from '#/logger'
@@ -59,7 +58,6 @@ import {ProfileScreen} from '#/view/screens/Profile'
 import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
 import {ProfileListScreen} from '#/view/screens/ProfileList'
 import {SavedFeeds} from '#/view/screens/SavedFeeds'
-import {SearchScreen} from '#/view/screens/Search'
 import {Storybook} from '#/view/screens/Storybook'
 import {SupportScreen} from '#/view/screens/Support'
 import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'
@@ -81,6 +79,7 @@ import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed'
 import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
 import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
 import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
+import {SearchScreen} from '#/screens/Search'
 import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
 import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
 import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx
index f20e517d4..665bbcba8 100644
--- a/src/components/FeedCard.tsx
+++ b/src/components/FeedCard.tsx
@@ -1,8 +1,8 @@
 import React from 'react'
-import {GestureResponderEvent, View} from 'react-native'
+import {type GestureResponderEvent, View} from 'react-native'
 import {
-  AppBskyFeedDefs,
-  AppBskyGraphDefs,
+  type AppBskyFeedDefs,
+  type AppBskyGraphDefs,
   AtUri,
   RichText as RichTextApi,
 } from '@atproto/api'
@@ -23,15 +23,20 @@ import * as Toast from '#/view/com/util/Toast'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {useTheme} from '#/alf'
 import {atoms as a} from '#/alf'
-import {Button, ButtonIcon} from '#/components/Button'
-import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
-import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
-import {Link as InternalLink, LinkProps} from '#/components/Link'
+import {
+  Button,
+  ButtonIcon,
+  type ButtonProps,
+  ButtonText,
+} from '#/components/Button'
+import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
+import {Link as InternalLink, type LinkProps} from '#/components/Link'
 import {Loader} from '#/components/Loader'
 import * as Prompt from '#/components/Prompt'
-import {RichText, RichTextProps} from '#/components/RichText'
+import {RichText, type RichTextProps} from '#/components/RichText'
 import {Text} from '#/components/Typography'
-import * as bsky from '#/types/bsky'
+import type * as bsky from '#/types/bsky'
+import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from './icons/Trash'
 
 type Props = {
   view: AppBskyFeedDefs.GeneratorView
@@ -81,11 +86,11 @@ export function Link({
 }
 
 export function Outer({children}: {children: React.ReactNode}) {
-  return <View style={[a.w_full, a.gap_md]}>{children}</View>
+  return <View style={[a.w_full, a.gap_sm]}>{children}</View>
 }
 
 export function Header({children}: {children: React.ReactNode}) {
-  return <View style={[a.flex_row, a.align_center, a.gap_md]}>{children}</View>
+  return <View style={[a.flex_row, a.align_center, a.gap_sm]}>{children}</View>
 }
 
 export type AvatarProps = {src: string | undefined; size?: number}
@@ -220,22 +225,27 @@ export function Likes({count}: {count: number}) {
 export function SaveButton({
   view,
   pin,
+  ...props
 }: {
   view: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView
   pin?: boolean
-}) {
+  text?: boolean
+} & Partial<ButtonProps>) {
   const {hasSession} = useSession()
   if (!hasSession) return null
-  return <SaveButtonInner view={view} pin={pin} />
+  return <SaveButtonInner view={view} pin={pin} {...props} />
 }
 
 function SaveButtonInner({
   view,
   pin,
+  text = true,
+  ...buttonProps
 }: {
   view: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView
   pin?: boolean
-}) {
+  text?: boolean
+} & Partial<ButtonProps>) {
   const {_} = useLingui()
   const {data: preferences} = usePreferencesQuery()
   const {isPending: isAddSavedFeedPending, mutateAsync: saveFeeds} =
@@ -294,14 +304,32 @@ function SaveButtonInner({
         disabled={isPending}
         label={_(msg`Add this feed to your feeds`)}
         size="small"
-        variant="ghost"
-        color="secondary"
-        shape="square"
-        onPress={savedFeedConfig ? onPrompRemoveFeed : toggleSave}>
+        variant="solid"
+        color={savedFeedConfig ? 'secondary' : 'primary'}
+        onPress={savedFeedConfig ? onPrompRemoveFeed : toggleSave}
+        {...buttonProps}>
         {savedFeedConfig ? (
-          <ButtonIcon size="md" icon={isPending ? Loader : Trash} />
+          <>
+            {isPending ? (
+              <ButtonIcon size="md" icon={Loader} />
+            ) : (
+              !text && <ButtonIcon size="md" icon={TrashIcon} />
+            )}
+            {text && (
+              <ButtonText>
+                <Trans>Unpin Feed</Trans>
+              </ButtonText>
+            )}
+          </>
         ) : (
-          <ButtonIcon size="md" icon={isPending ? Loader : Plus} />
+          <>
+            <ButtonIcon size="md" icon={isPending ? Loader : PinIcon} />
+            {text && (
+              <ButtonText>
+                <Trans>Pin Feed</Trans>
+              </ButtonText>
+            )}
+          </>
         )}
       </Button>
 
diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index b56112dcf..a37432500 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -1,14 +1,14 @@
 import React from 'react'
-import {GestureResponderEvent, View} from 'react-native'
+import {type GestureResponderEvent, View} from 'react-native'
 import {
   moderateProfile,
-  ModerationOpts,
+  type ModerationOpts,
   RichText as RichTextApi,
 } from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {LogEvents} from '#/lib/statsig/statsig'
+import {type LogEvents} from '#/lib/statsig/statsig'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -18,13 +18,18 @@ import {ProfileCardPills} from '#/view/com/profile/ProfileCard'
 import * as Toast from '#/view/com/util/Toast'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {atoms as a, useTheme} from '#/alf'
-import {Button, ButtonIcon, ButtonProps, ButtonText} from '#/components/Button'
+import {
+  Button,
+  ButtonIcon,
+  type ButtonProps,
+  ButtonText,
+} from '#/components/Button'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
-import {Link as InternalLink, LinkProps} from '#/components/Link'
+import {Link as InternalLink, type LinkProps} from '#/components/Link'
 import {RichText} from '#/components/RichText'
 import {Text} from '#/components/Typography'
-import * as bsky from '#/types/bsky'
+import type * as bsky from '#/types/bsky'
 
 export function Default({
   profile,
@@ -133,7 +138,7 @@ export function Avatar({
 
   return (
     <UserAvatar
-      size={42}
+      size={40}
       avatar={profile.avatar}
       type={profile.associated?.labeler ? 'labeler' : 'user'}
       moderation={moderation.ui('avatar')}
@@ -149,8 +154,8 @@ export function AvatarPlaceholder() {
         a.rounded_full,
         t.atoms.bg_contrast_50,
         {
-          width: 42,
-          height: 42,
+          width: 40,
+          height: 40,
         },
       ]}
     />
@@ -261,7 +266,7 @@ export function DescriptionPlaceholder({
 }) {
   const t = useTheme()
   return (
-    <View style={[{gap: 8}]}>
+    <View style={[a.pt_2xs, {gap: 6}]}>
       {Array(numberOfLines)
         .fill(0)
         .map((_, i) => (
@@ -286,6 +291,7 @@ export type FollowButtonProps = {
     LogEvents['profile:unfollow']['logContext']
   colorInverted?: boolean
   onFollow?: () => void
+  withIcon?: boolean
 } & Partial<ButtonProps>
 
 export function FollowButton(props: FollowButtonProps) {
@@ -301,6 +307,7 @@ export function FollowButtonInner({
   onPress: onPressProp,
   onFollow,
   colorInverted,
+  withIcon = true,
   ...rest
 }: FollowButtonProps) {
   const {_} = useLingui()
@@ -386,7 +393,9 @@ export function FollowButtonInner({
           color="secondary"
           {...rest}
           onPress={onPressUnfollow}>
-          <ButtonIcon icon={Check} position={isRound ? undefined : 'left'} />
+          {withIcon && (
+            <ButtonIcon icon={Check} position={isRound ? undefined : 'left'} />
+          )}
           {isRound ? null : <ButtonText>{unfollowLabel}</ButtonText>}
         </Button>
       ) : (
@@ -397,7 +406,9 @@ export function FollowButtonInner({
           color={colorInverted ? 'secondary_inverted' : 'primary'}
           {...rest}
           onPress={onPressFollow}>
-          <ButtonIcon icon={Plus} position={isRound ? undefined : 'left'} />
+          {withIcon && (
+            <ButtonIcon icon={Plus} position={isRound ? undefined : 'left'} />
+          )}
           {isRound ? null : <ButtonText>{followLabel}</ButtonText>}
         </Button>
       )}
diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx
index 006f86574..41c3d41d8 100644
--- a/src/components/ProgressGuide/FollowDialog.tsx
+++ b/src/components/ProgressGuide/FollowDialog.tsx
@@ -5,7 +5,7 @@ import Animated, {
   LinearTransition,
   ZoomInEasyDown,
 } from 'react-native-reanimated'
-import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
+import {type AppBskyActorDefs, type ModerationOpts} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -19,8 +19,8 @@ import {useActorSearchPaginated} from '#/state/queries/actor-search'
 import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
 import {useSession} from '#/state/session'
-import {Follow10ProgressGuide} from '#/state/shell/progress-guide'
-import {ListMethods} from '#/view/com/util/List'
+import {type Follow10ProgressGuide} from '#/state/shell/progress-guide'
+import {type ListMethods} from '#/view/com/util/List'
 import {
   popularInterests,
   useInterestsDisplayNames,
@@ -31,7 +31,7 @@ import {
   tokens,
   useBreakpoints,
   useTheme,
-  ViewStyleProp,
+  type ViewStyleProp,
   web,
 } from '#/alf'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -452,12 +452,14 @@ let Tabs = ({
   selectedInterest,
   hasSearchText,
   interestsDisplayNames,
+  TabComponent = Tab,
 }: {
   onSelectTab: (tab: string) => void
   interests: string[]
   selectedInterest: string
   hasSearchText: boolean
   interestsDisplayNames: Record<string, string>
+  TabComponent?: React.ComponentType<React.ComponentProps<typeof Tab>>
 }): React.ReactNode => {
   const listRef = useRef<ScrollView>(null)
   const [scrollX, setScrollX] = useState(0)
@@ -532,7 +534,7 @@ let Tabs = ({
       {interests.map((interest, i) => {
         const active = interest === selectedInterest && !hasSearchText
         return (
-          <Tab
+          <TabComponent
             key={interest}
             onSelectTab={handleSelectTab}
             active={active}
@@ -547,6 +549,7 @@ let Tabs = ({
   )
 }
 Tabs = memo(Tabs)
+export {Tabs}
 
 let Tab = ({
   onSelectTab,
@@ -822,7 +825,7 @@ function Empty({message}: {message: string}) {
   )
 }
 
-function boostInterests(boosts?: string[]) {
+export function boostInterests(boosts?: string[]) {
   return (_a: string, _b: string) => {
     const indexA = boosts?.indexOf(_a) ?? -1
     const indexB = boosts?.indexOf(_b) ?? -1
diff --git a/src/components/StarterPack/StarterPackCard.tsx b/src/components/StarterPack/StarterPackCard.tsx
index b5760fd6d..88d075b78 100644
--- a/src/components/StarterPack/StarterPackCard.tsx
+++ b/src/components/StarterPack/StarterPackCard.tsx
@@ -13,7 +13,10 @@ import {precacheStarterPack} from '#/state/queries/starter-packs'
 import {useSession} from '#/state/session'
 import {atoms as a, useTheme} from '#/alf'
 import {StarterPack as StarterPackIcon} from '#/components/icons/StarterPack'
-import {Link as BaseLink, LinkProps as BaseLinkProps} from '#/components/Link'
+import {
+  Link as BaseLink,
+  type LinkProps as BaseLinkProps,
+} from '#/components/Link'
 import {Text} from '#/components/Typography'
 import * as bsky from '#/types/bsky'
 
@@ -104,6 +107,32 @@ export function Card({
   )
 }
 
+export function useStarterPackLink({
+  view,
+}: {
+  view: bsky.starterPack.AnyStarterPackView
+}) {
+  const {_} = useLingui()
+  const qc = useQueryClient()
+  const {rkey, handleOrDid} = React.useMemo(() => {
+    const rkey = new AtUri(view.uri).rkey
+    const {creator} = view
+    return {rkey, handleOrDid: creator.handle || creator.did}
+  }, [view])
+  const precache = () => {
+    precacheResolvedUri(qc, view.creator.handle, view.creator.did)
+    precacheStarterPack(qc, view)
+  }
+
+  return {
+    to: `/starter-pack/${handleOrDid}/${rkey}`,
+    label: AppBskyGraphStarterpack.isRecord(view.record)
+      ? _(msg`Navigate to ${view.record.name}`)
+      : _(msg`Navigate to starter pack`),
+    precache,
+  }
+}
+
 export function Link({
   starterPack,
   children,
diff --git a/src/components/icons/Flame.tsx b/src/components/icons/Flame.tsx
new file mode 100644
index 000000000..42569b0de
--- /dev/null
+++ b/src/components/icons/Flame.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const Flame_Stroke2_Corner1_Rounded = createSinglePathSVG({
+  path: 'M11.158 2.879c.584-.835 1.757-1.137 2.673-.507.951.654 2.597 1.92 4.013 3.694S20.5 10.194 20.5 13c0 4.997-3.752 9-8.5 9s-8.5-4.003-8.5-9c0-2.035.874-4.636 2.578-6.712.746-.91 2.034-.855 2.786-.133l2.294-3.276Zm-3.04 15.758C6.538 17.386 5.5 15.37 5.5 13c0-1.511.666-3.616 2.042-5.342.87.797 2.254.653 2.939-.325l2.286-3.265c.871.606 2.299 1.723 3.514 3.246C17.53 8.879 18.5 10.804 18.5 13c0 2.369-1.038 4.386-2.618 5.637q.117-.518.118-1.061c0-2.601-2.038-4.382-2.911-5.04a1.8 1.8 0 0 0-2.177 0C10.038 13.195 8 14.976 8 17.577q0 .543.118 1.061ZM12 14.222c-.825.648-2 1.859-2 3.354C10 19.043 11.016 20 12 20s2-.957 2-2.424c0-1.495-1.175-2.706-2-3.354Z',
+})
diff --git a/src/components/icons/Trending2.tsx b/src/components/icons/Trending.tsx
index 5fba4167b..bdc8539e0 100644
--- a/src/components/icons/Trending2.tsx
+++ b/src/components/icons/Trending.tsx
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
 export const Trending2_Stroke2_Corner2_Rounded = createSinglePathSVG({
   path: 'm18.192 5.004 1.864 5.31a1 1 0 0 0 1.887-.662L20.08 4.34c-.665-1.893-3.378-1.741-3.834.207l-3.381 14.449-2.985-9.605C9.3 7.531 6.684 7.506 6.07 9.355l-1.18 3.56-.969-2.312a1 1 0 0 0-1.844.772l.97 2.315c.715 1.71 3.159 1.613 3.741-.144l1.18-3.56 2.985 9.605c.607 1.952 3.392 1.848 3.857-.138l3.381-14.449Z',
 })
+
+export const Trending3_Stroke2_Corner1_Rounded = createSinglePathSVG({
+  path: 'M15 7a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V9.414L14.414 15a2 2 0 0 1-2.828 0L9 12.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L7.586 11a2 2 0 0 1 2.828 0L13 13.586 18.586 8H16a1 1 0 0 1-1-1Z',
+})
diff --git a/src/components/icons/common.tsx b/src/components/icons/common.tsx
index 996ecb626..bc1e045a4 100644
--- a/src/components/icons/common.tsx
+++ b/src/components/icons/common.tsx
@@ -1,5 +1,5 @@
-import {StyleSheet, TextProps} from 'react-native'
-import type {PathProps, SvgProps} from 'react-native-svg'
+import {StyleSheet, type TextProps} from 'react-native'
+import {type PathProps, type SvgProps} from 'react-native-svg'
 import {Defs, LinearGradient, Stop} from 'react-native-svg'
 import {nanoid} from 'nanoid/non-secure'
 
@@ -19,7 +19,7 @@ export const sizes = {
   lg: 24,
   xl: 28,
   '2xl': 32,
-}
+} as const
 
 export function useCommonSVGProps(props: Props) {
   const t = useTheme()
diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx
index 7412c6f0a..56c756c50 100644
--- a/src/components/interstitials/Trending.tsx
+++ b/src/components/interstitials/Trending.tsx
@@ -15,7 +15,7 @@ import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
 import {atoms as a, useGutters, useTheme} from '#/alf'
 import {Button, ButtonIcon} from '#/components/Button'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
-import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
+import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
 import * as Prompt from '#/components/Prompt'
 import {TrendingTopicLink} from '#/components/TrendingTopics'
 import {Text} from '#/components/Typography'
diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx
index 126d6f417..fab738b9c 100644
--- a/src/components/interstitials/TrendingVideos.tsx
+++ b/src/components/interstitials/TrendingVideos.tsx
@@ -16,7 +16,7 @@ import {atoms as a, useGutters, useTheme} from '#/alf'
 import {Button, ButtonIcon} from '#/components/Button'
 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
-import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
+import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
 import {Link} from '#/components/Link'
 import * as Prompt from '#/components/Prompt'
 import {Text} from '#/components/Typography'
diff --git a/src/lib/icons.tsx b/src/lib/icons.tsx
index 3690783d3..6e0be9d0a 100644
--- a/src/lib/icons.tsx
+++ b/src/lib/icons.tsx
@@ -1,4 +1,4 @@
-import {StyleProp, TextStyle, ViewStyle} from 'react-native'
+import {type StyleProp, type TextStyle, type ViewStyle} from 'react-native'
 import Svg, {Ellipse, Line, Path, Rect} from 'react-native-svg'
 
 // Copyright (c) 2020 Refactoring UI Inc.
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index c88a97c75..d3334d82f 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -2,6 +2,7 @@ export type Gate =
   // Keep this alphabetic please.
   | 'debug_show_feedcontext'
   | 'debug_subscriptions'
+  | 'explore_show_suggested_feeds'
   | 'old_postonboarding'
   | 'onboarding_add_video_feed'
   | 'remove_show_latest_button'
diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts
index 33cdc25e5..646758369 100644
--- a/src/logger/metrics.ts
+++ b/src/logger/metrics.ts
@@ -1,3 +1,5 @@
+import {type FeedDescriptor} from '#/state/queries/post-feed'
+
 export type MetricEvents = {
   // App events
   init: {
@@ -202,6 +204,7 @@ export type MetricEvents = {
       | 'ProfileHeaderSuggestedFollows'
       | 'PostOnboardingFindFollows'
       | 'ImmersiveVideo'
+      | 'ExploreSuggestedAccounts'
   }
   'suggestedUser:follow': {
     logContext:
@@ -239,6 +242,7 @@ export type MetricEvents = {
       | 'ProfileHeaderSuggestedFollows'
       | 'PostOnboardingFindFollows'
       | 'ImmersiveVideo'
+      | 'ExploreSuggestedAccounts'
   }
   'chat:create': {
     logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
@@ -318,6 +322,22 @@ export type MetricEvents = {
     context: 'interstitial:discover' | 'interstitial:explore' | 'feed'
   }
 
+  'explore:module:seen': {
+    module:
+      | 'trendingTopics'
+      | 'trendingVideos'
+      | 'suggestedAccounts'
+      | 'suggestedFeeds'
+      | 'suggestedStarterPacks'
+      | `feed:${FeedDescriptor}`
+  }
+  'explore:module:searchButtonPress': {
+    module: 'suggestedAccounts' | 'suggestedFeeds'
+  }
+  'explore:suggestedAccounts:tabPressed': {
+    tab: string
+  }
+
   'progressGuide:hide': {}
   'progressGuide:followDialog:open': {}
 
diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx
index d0b0cacca..e725b7b80 100644
--- a/src/screens/Onboarding/StepFinished.tsx
+++ b/src/screens/Onboarding/StepFinished.tsx
@@ -1,12 +1,12 @@
 import React from 'react'
 import {View} from 'react-native'
 import {
-  AppBskyActorProfile,
-  AppBskyGraphDefs,
+  type AppBskyActorProfile,
+  type AppBskyGraphDefs,
   AppBskyGraphStarterpack,
-  Un$Typed,
+  type Un$Typed,
 } from '@atproto/api'
-import {SavedFeed} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
+import {type SavedFeed} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
 import {TID} from '@atproto/common-web'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -46,7 +46,7 @@ import {IconCircle} from '#/components/IconCircle'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
 import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
 import {News2_Stroke2_Corner0_Rounded as News} from '#/components/icons/News2'
-import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2'
+import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending'
 import {Loader} from '#/components/Loader'
 import {Text} from '#/components/Typography'
 import * as bsky from '#/types/bsky'
diff --git a/src/screens/Profile/ProfileSearch.tsx b/src/screens/Profile/ProfileSearch.tsx
index d91dc973e..6247e3979 100644
--- a/src/screens/Profile/ProfileSearch.tsx
+++ b/src/screens/Profile/ProfileSearch.tsx
@@ -2,11 +2,14 @@ import {useMemo} from 'react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {
+  type CommonNavigatorParams,
+  type NativeStackScreenProps,
+} from '#/lib/routes/types'
 import {useProfileQuery} from '#/state/queries/profile'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {useSession} from '#/state/session'
-import {SearchScreenShell} from '#/view/screens/Search/Search'
+import {SearchScreenShell} from '#/screens/Search/Shell'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileSearch'>
 export const ProfileSearchScreen = ({route}: Props) => {
diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx
new file mode 100644
index 000000000..86877677a
--- /dev/null
+++ b/src/screens/Search/Explore.tsx
@@ -0,0 +1,923 @@
+import {useCallback, useMemo, useRef, useState} from 'react'
+import {View, type ViewabilityConfig, type ViewToken} from 'react-native'
+import {
+  type AppBskyActorDefs,
+  type AppBskyFeedDefs,
+  type AppBskyGraphDefs,
+} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useGate} from '#/lib/statsig/statsig'
+import {cleanError} from '#/lib/strings/errors'
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {logger} from '#/logger'
+import {type MetricEvents} from '#/logger/metrics'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {useActorSearchPaginated} from '#/state/queries/actor-search'
+import {useGetPopularFeedsQuery} from '#/state/queries/feed'
+import {usePreferencesQuery} from '#/state/queries/preferences'
+import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
+import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
+import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery'
+import {useProgressGuide} from '#/state/shell/progress-guide'
+import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
+import {PostFeedItem} from '#/view/com/posts/PostFeedItem'
+import {ViewFullThread} from '#/view/com/posts/ViewFullThread'
+import {List} from '#/view/com/util/List'
+import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
+import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
+import {
+  StarterPackCard,
+  StarterPackCardSkeleton,
+} from '#/screens/Search/components/StarterPackCard'
+import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations'
+import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
+import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
+import {atoms as a, native, useTheme, web} from '#/alf'
+import {Button} from '#/components/Button'
+import * as FeedCard from '#/components/FeedCard'
+import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
+import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import {type Props as SVGIconProps} from '#/components/icons/common'
+import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
+import {StarterPack} from '#/components/icons/StarterPack'
+import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
+import {Loader} from '#/components/Loader'
+import * as ProfileCard from '#/components/ProfileCard'
+import {Text} from '#/components/Typography'
+import * as ModuleHeader from './components/ModuleHeader'
+import {
+  type FeedPreviewItem,
+  useFeedPreviews,
+} from './modules/ExploreFeedPreviews'
+import {
+  SuggestedAccountsTabBar,
+  SuggestedProfileCard,
+  useLoadEnoughProfiles,
+} from './modules/ExploreSuggestedAccounts'
+
+function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
+  const t = useTheme()
+  const {_} = useLingui()
+
+  return (
+    <Button
+      label={_(msg`Load more`)}
+      onPress={item.onLoadMore}
+      style={[a.relative, a.w_full]}>
+      {({hovered, pressed}) => (
+        <View
+          style={[
+            a.flex_1,
+            a.flex_row,
+            a.align_center,
+            a.justify_center,
+            a.px_lg,
+            a.py_md,
+            a.gap_sm,
+            (hovered || pressed) && t.atoms.bg_contrast_25,
+          ]}>
+          <Text
+            style={[
+              a.leading_snug,
+              hovered ? t.atoms.text : t.atoms.text_contrast_medium,
+            ]}>
+            {item.message}
+          </Text>
+          {item.isLoadingMore ? (
+            <Loader size="sm" />
+          ) : (
+            <ChevronDownIcon
+              size="sm"
+              style={hovered ? t.atoms.text : t.atoms.text_contrast_medium}
+            />
+          )}
+        </View>
+      )}
+    </Button>
+  )
+}
+
+type ExploreScreenItems =
+  | {
+      type: 'topBorder'
+      key: string
+    }
+  | {
+      type: 'header'
+      key: string
+      title: string
+      icon: React.ComponentType<SVGIconProps>
+      searchButton?: {
+        label: string
+        metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
+        tab: 'user' | 'profile' | 'feed'
+      }
+    }
+  | {
+      type: 'tabbedHeader'
+      key: string
+      title: string
+      icon: React.ComponentType<SVGIconProps>
+      searchButton?: {
+        label: string
+        metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
+        tab: 'user' | 'profile' | 'feed'
+      }
+    }
+  | {
+      type: 'trendingTopics'
+      key: string
+    }
+  | {
+      type: 'trendingVideos'
+      key: string
+    }
+  | {
+      type: 'recommendations'
+      key: string
+    }
+  | {
+      type: 'profile'
+      key: string
+      profile: AppBskyActorDefs.ProfileView
+      recId?: number
+    }
+  | {
+      type: 'feed'
+      key: string
+      feed: AppBskyFeedDefs.GeneratorView
+    }
+  | {
+      type: 'loadMore'
+      key: string
+      message: string
+      isLoadingMore: boolean
+      onLoadMore: () => void
+    }
+  | {
+      type: 'profilePlaceholder'
+      key: string
+    }
+  | {
+      type: 'feedPlaceholder'
+      key: string
+    }
+  | {
+      type: 'error'
+      key: string
+      message: string
+      error: string
+    }
+  | {
+      type: 'starterPack'
+      key: string
+      view: AppBskyGraphDefs.StarterPackView
+    }
+  | {
+      type: 'starterPackSkeleton'
+      key: string
+    }
+  | FeedPreviewItem
+
+export function Explore({
+  focusSearchInput,
+  headerHeight,
+}: {
+  focusSearchInput: (tab: 'user' | 'profile' | 'feed') => void
+  headerHeight: number
+}) {
+  const {_} = useLingui()
+  const t = useTheme()
+  const {data: preferences, error: preferencesError} = usePreferencesQuery()
+  const moderationOpts = useModerationOpts()
+  const gate = useGate()
+  const guide = useProgressGuide('follow-10')
+  const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
+  const {
+    data: suggestedProfiles,
+    hasNextPage: hasNextSuggestedProfilesPage,
+    isLoading: isLoadingSuggestedProfiles,
+    isFetchingNextPage: isFetchingNextSuggestedProfilesPage,
+    error: suggestedProfilesError,
+    fetchNextPage: fetchNextSuggestedProfilesPage,
+  } = useSuggestedFollowsQuery({limit: 3, subsequentPageLimit: 10})
+  const {
+    data: interestProfiles,
+    hasNextPage: hasNextInterestProfilesPage,
+    isLoading: isLoadingInterestProfiles,
+    isFetchingNextPage: isFetchingNextInterestProfilesPage,
+    error: interestProfilesError,
+    fetchNextPage: fetchNextInterestProfilesPage,
+  } = useActorSearchPaginated({
+    query: selectedInterest || '',
+    enabled: !!selectedInterest,
+    limit: 10,
+  })
+  const {isReady: canShowSuggestedProfiles} = useLoadEnoughProfiles({
+    interest: selectedInterest,
+    data: interestProfiles,
+    isLoading: isLoadingInterestProfiles,
+    isFetchingNextPage: isFetchingNextInterestProfilesPage,
+    hasNextPage: hasNextInterestProfilesPage,
+    fetchNextPage: fetchNextInterestProfilesPage,
+  })
+  const {
+    data: feeds,
+    hasNextPage: hasNextFeedsPage,
+    isLoading: isLoadingFeeds,
+    isFetchingNextPage: isFetchingNextFeedsPage,
+    error: feedsError,
+    fetchNextPage: fetchNextFeedsPage,
+  } = useGetPopularFeedsQuery({limit: 10})
+
+  const profiles: typeof suggestedProfiles & typeof interestProfiles =
+    !selectedInterest ? suggestedProfiles : interestProfiles
+  const hasNextProfilesPage = !selectedInterest
+    ? hasNextSuggestedProfilesPage
+    : hasNextInterestProfilesPage
+  const isLoadingProfiles = !selectedInterest
+    ? isLoadingSuggestedProfiles
+    : !canShowSuggestedProfiles
+  const isFetchingNextProfilesPage = !selectedInterest
+    ? isFetchingNextSuggestedProfilesPage
+    : !canShowSuggestedProfiles
+  const profilesError = !selectedInterest
+    ? suggestedProfilesError
+    : interestProfilesError
+  const fetchNextProfilesPage = !selectedInterest
+    ? fetchNextSuggestedProfilesPage
+    : fetchNextInterestProfilesPage
+
+  const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
+  const onLoadMoreProfiles = useCallback(async () => {
+    if (isFetchingNextProfilesPage || !hasNextProfilesPage || profilesError)
+      return
+    try {
+      await fetchNextProfilesPage()
+    } catch (err) {
+      logger.error('Failed to load more suggested follows', {message: err})
+    }
+  }, [
+    isFetchingNextProfilesPage,
+    hasNextProfilesPage,
+    profilesError,
+    fetchNextProfilesPage,
+  ])
+  const {
+    data: suggestedSPs,
+    isLoading: isLoadingSuggestedSPs,
+    error: suggestedSPsError,
+  } = useSuggestedStarterPacksQuery()
+
+  const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
+  const [hasPressedLoadMoreFeeds, setHasPressedLoadMoreFeeds] = useState(false)
+  const onLoadMoreFeeds = useCallback(async () => {
+    if (isFetchingNextFeedsPage || !hasNextFeedsPage || feedsError) return
+    if (!hasPressedLoadMoreFeeds) {
+      setHasPressedLoadMoreFeeds(true)
+      return
+    }
+    try {
+      await fetchNextFeedsPage()
+    } catch (err) {
+      logger.error('Failed to load more suggested follows', {message: err})
+    }
+  }, [
+    isFetchingNextFeedsPage,
+    hasNextFeedsPage,
+    feedsError,
+    fetchNextFeedsPage,
+    hasPressedLoadMoreFeeds,
+  ])
+
+  const {data: suggestedFeeds} = useGetSuggestedFeedsQuery()
+  const {
+    data: feedPreviewSlices,
+    query: {
+      isPending: isPendingFeedPreviews,
+      isFetchingNextPage: isFetchingNextPageFeedPreviews,
+      fetchNextPage: fetchNextPageFeedPreviews,
+      hasNextPage: hasNextPageFeedPreviews,
+      error: feedPreviewSlicesError,
+    },
+  } = useFeedPreviews(suggestedFeeds?.feeds ?? [])
+
+  const onLoadMoreFeedPreviews = useCallback(async () => {
+    if (
+      isPendingFeedPreviews ||
+      isFetchingNextPageFeedPreviews ||
+      !hasNextPageFeedPreviews ||
+      feedPreviewSlicesError
+    )
+      return
+    try {
+      await fetchNextPageFeedPreviews()
+    } catch (err) {
+      logger.error('Failed to load more feed previews', {message: err})
+    }
+  }, [
+    isPendingFeedPreviews,
+    isFetchingNextPageFeedPreviews,
+    hasNextPageFeedPreviews,
+    feedPreviewSlicesError,
+    fetchNextPageFeedPreviews,
+  ])
+
+  const items = useMemo<ExploreScreenItems[]>(() => {
+    const i: ExploreScreenItems[] = []
+
+    const addTopBorder = () => {
+      i.push({type: 'topBorder', key: 'top-border'})
+    }
+
+    const addTrendingTopicsModule = () => {
+      i.push({
+        type: 'trendingTopics',
+        key: `trending-topics`,
+      })
+
+      // temp - disable trending videos
+      // if (isNative) {
+      //   i.push({
+      //     type: 'trendingVideos',
+      //     key: `trending-videos`,
+      //   })
+      // }
+    }
+
+    const addSuggestedFollowsModule = () => {
+      i.push({
+        type: 'tabbedHeader',
+        key: 'suggested-accounts-header',
+        title: _(msg`Suggested Accounts`),
+        icon: Person,
+        searchButton: {
+          label: _(msg`Search for more accounts`),
+          metricsTag: 'suggestedAccounts',
+          tab: 'user',
+        },
+      })
+
+      if (!canShowSuggestedProfiles) {
+        i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
+      } else if (profilesError) {
+        i.push({
+          type: 'error',
+          key: 'profilesError',
+          message: _(msg`Failed to load suggested follows`),
+          error: cleanError(profilesError),
+        })
+      } else {
+        if (profiles !== undefined) {
+          if (profiles.pages.length > 0 && moderationOpts) {
+            // Currently the responses contain duplicate items.
+            // Needs to be fixed on backend, but let's dedupe to be safe.
+            let seen = new Set()
+            const profileItems: ExploreScreenItems[] = []
+            for (const page of profiles.pages) {
+              for (const actor of page.actors) {
+                if (!seen.has(actor.did) && !actor.viewer?.following) {
+                  seen.add(actor.did)
+                  profileItems.push({
+                    type: 'profile',
+                    key: actor.did,
+                    profile: actor,
+                    recId: page.recId,
+                  })
+                }
+              }
+            }
+
+            if (profileItems.length === 0) {
+              if (!hasNextProfilesPage) {
+                // no items! remove the header
+                i.pop()
+              }
+            } else {
+              i.push(...profileItems)
+            }
+            if (hasNextProfilesPage) {
+              i.push({
+                type: 'loadMore',
+                key: 'loadMoreProfiles',
+                message: _(msg`Load more suggested accounts`),
+                isLoadingMore: isLoadingMoreProfiles,
+                onLoadMore: onLoadMoreProfiles,
+              })
+            }
+          } else {
+            console.log('no pages')
+          }
+        } else {
+          i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
+        }
+      }
+    }
+
+    const addSuggestedFeedsModule = () => {
+      i.push({
+        type: 'header',
+        key: 'suggested-feeds-header',
+        title: _(msg`Discover Feeds`),
+        icon: ListSparkle,
+        searchButton: {
+          label: _(msg`Search for more feeds`),
+          metricsTag: 'suggestedFeeds',
+          tab: 'feed',
+        },
+      })
+
+      if (feeds && preferences) {
+        // Currently the responses contain duplicate items.
+        // Needs to be fixed on backend, but let's dedupe to be safe.
+        let seen = new Set()
+        const feedItems: ExploreScreenItems[] = []
+        for (const page of feeds.pages) {
+          for (const feed of page.feeds) {
+            if (!seen.has(feed.uri)) {
+              seen.add(feed.uri)
+              feedItems.push({
+                type: 'feed',
+                key: feed.uri,
+                feed,
+              })
+            }
+          }
+        }
+
+        // feeds errors can occur during pagination, so feeds is truthy
+        if (feedsError) {
+          i.push({
+            type: 'error',
+            key: 'feedsError',
+            message: _(msg`Failed to load suggested feeds`),
+            error: cleanError(feedsError),
+          })
+        } else if (preferencesError) {
+          i.push({
+            type: 'error',
+            key: 'preferencesError',
+            message: _(msg`Failed to load feeds preferences`),
+            error: cleanError(preferencesError),
+          })
+        } else {
+          if (feedItems.length === 0) {
+            if (!hasNextFeedsPage) {
+              i.pop()
+            }
+          } else {
+            // This query doesn't follow the limit very well, so the first press of the
+            // load more button just unslices the array back to ~10 items
+            if (!hasPressedLoadMoreFeeds) {
+              i.push(...feedItems.slice(0, 3))
+            } else {
+              i.push(...feedItems)
+            }
+          }
+          if (hasNextFeedsPage) {
+            i.push({
+              type: 'loadMore',
+              key: 'loadMoreFeeds',
+              message: _(msg`Load more suggested feeds`),
+              isLoadingMore: isLoadingMoreFeeds,
+              onLoadMore: onLoadMoreFeeds,
+            })
+          }
+        }
+      } else {
+        if (feedsError) {
+          i.push({
+            type: 'error',
+            key: 'feedsError',
+            message: _(msg`Failed to load suggested feeds`),
+            error: cleanError(feedsError),
+          })
+        } else if (preferencesError) {
+          i.push({
+            type: 'error',
+            key: 'preferencesError',
+            message: _(msg`Failed to load feeds preferences`),
+            error: cleanError(preferencesError),
+          })
+        } else {
+          i.push({type: 'feedPlaceholder', key: 'feedPlaceholder'})
+        }
+      }
+    }
+
+    const addSuggestedStarterPacksModule = () => {
+      i.push({
+        type: 'header',
+        key: 'suggested-starterPacks-header',
+        title: _(msg`Starter Packs`),
+        icon: StarterPack,
+      })
+
+      if (isLoadingSuggestedSPs) {
+        Array.from({length: 3}).forEach((_, index) =>
+          i.push({
+            type: 'starterPackSkeleton',
+            key: `starterPackSkeleton-${index}`,
+          }),
+        )
+      } else if (suggestedSPsError || !suggestedSPs) {
+        // just get rid of the section
+        i.pop()
+      } else {
+        suggestedSPs.starterPacks.map(s => {
+          i.push({
+            type: 'starterPack',
+            key: s.uri,
+            view: s,
+          })
+        })
+      }
+    }
+
+    const addFeedPreviews = () => {
+      i.push(...feedPreviewSlices)
+      if (isFetchingNextPageFeedPreviews) {
+        i.push({
+          type: 'preview:loading',
+          key: 'preview-loading-more',
+        })
+      }
+    }
+
+    // Dynamic module ordering
+
+    addTopBorder()
+
+    if (guide?.guide === 'follow-10' && !guide.isComplete) {
+      addSuggestedFollowsModule()
+      addSuggestedStarterPacksModule()
+      addTrendingTopicsModule()
+    } else {
+      addTrendingTopicsModule()
+      addSuggestedFollowsModule()
+      addSuggestedStarterPacksModule()
+    }
+
+    if (gate('explore_show_suggested_feeds')) {
+      addSuggestedFeedsModule()
+    }
+
+    addFeedPreviews()
+
+    return i
+  }, [
+    _,
+    profiles,
+    feeds,
+    preferences,
+    onLoadMoreFeeds,
+    onLoadMoreProfiles,
+    isLoadingMoreProfiles,
+    isLoadingMoreFeeds,
+    profilesError,
+    feedsError,
+    preferencesError,
+    hasNextProfilesPage,
+    hasNextFeedsPage,
+    guide,
+    gate,
+    moderationOpts,
+    hasPressedLoadMoreFeeds,
+    suggestedSPs,
+    isLoadingSuggestedSPs,
+    suggestedSPsError,
+    feedPreviewSlices,
+    isFetchingNextPageFeedPreviews,
+    canShowSuggestedProfiles,
+  ])
+
+  const renderItem = useCallback(
+    ({item, index}: {item: ExploreScreenItems; index: number}) => {
+      switch (item.type) {
+        case 'topBorder':
+          return (
+            <View
+              style={[
+                a.w_full,
+                t.atoms.border_contrast_low,
+                a.border_t,
+                headerHeight &&
+                  web({
+                    position: 'sticky',
+                    top: headerHeight,
+                  }),
+              ]}
+            />
+          )
+        case 'header': {
+          return (
+            <ModuleHeader.Container>
+              <ModuleHeader.Icon icon={item.icon} />
+              <ModuleHeader.TitleText>{item.title}</ModuleHeader.TitleText>
+              {item.searchButton && (
+                <ModuleHeader.SearchButton
+                  {...item.searchButton}
+                  onPress={() =>
+                    focusSearchInput(item.searchButton?.tab || 'user')
+                  }
+                />
+              )}
+            </ModuleHeader.Container>
+          )
+        }
+        case 'tabbedHeader': {
+          return (
+            <View style={[a.pb_md]}>
+              <ModuleHeader.Container style={[a.pb_xs]}>
+                <ModuleHeader.Icon icon={item.icon} />
+                <ModuleHeader.TitleText>{item.title}</ModuleHeader.TitleText>
+                {item.searchButton && (
+                  <ModuleHeader.SearchButton
+                    {...item.searchButton}
+                    onPress={() =>
+                      focusSearchInput(item.searchButton?.tab || 'user')
+                    }
+                  />
+                )}
+              </ModuleHeader.Container>
+              <SuggestedAccountsTabBar
+                selectedInterest={selectedInterest}
+                onSelectInterest={setSelectedInterest}
+              />
+            </View>
+          )
+        }
+        case 'trendingTopics': {
+          return (
+            <View style={[a.pb_md]}>
+              <ExploreTrendingTopics />
+            </View>
+          )
+        }
+        case 'trendingVideos': {
+          return <ExploreTrendingVideos />
+        }
+        case 'recommendations': {
+          return <ExploreRecommendations />
+        }
+        case 'profile': {
+          return (
+            <SuggestedProfileCard
+              profile={item.profile}
+              moderationOpts={moderationOpts!}
+              recId={item.recId}
+              position={index}
+            />
+          )
+        }
+        case 'feed': {
+          return (
+            <View
+              style={[
+                a.border_t,
+                t.atoms.border_contrast_low,
+                a.px_lg,
+                a.py_lg,
+              ]}>
+              <FeedCard.Default view={item.feed} />
+            </View>
+          )
+        }
+        case 'starterPack': {
+          return (
+            <View style={[a.px_lg, a.pb_lg]}>
+              <StarterPackCard view={item.view} />
+            </View>
+          )
+        }
+        case 'starterPackSkeleton': {
+          return (
+            <View style={[a.px_lg, a.pb_lg]}>
+              <StarterPackCardSkeleton />
+            </View>
+          )
+        }
+        case 'loadMore': {
+          return (
+            <View style={[a.border_t, t.atoms.border_contrast_low]}>
+              <LoadMore item={item} />
+            </View>
+          )
+        }
+        case 'profilePlaceholder': {
+          return (
+            <>
+              {Array.from({length: 3}).map((_, index) => (
+                <View
+                  style={[
+                    a.px_lg,
+                    a.py_lg,
+                    a.border_t,
+                    t.atoms.border_contrast_low,
+                  ]}
+                  key={index}>
+                  <ProfileCard.Outer>
+                    <ProfileCard.Header>
+                      <ProfileCard.AvatarPlaceholder />
+                      <ProfileCard.NameAndHandlePlaceholder />
+                    </ProfileCard.Header>
+                    <ProfileCard.DescriptionPlaceholder numberOfLines={2} />
+                  </ProfileCard.Outer>
+                </View>
+              ))}
+            </>
+          )
+        }
+        case 'feedPlaceholder': {
+          return <FeedFeedLoadingPlaceholder />
+        }
+        case 'error':
+        case 'preview:error': {
+          return (
+            <View
+              style={[
+                a.border_t,
+                a.pt_md,
+                a.px_md,
+                t.atoms.border_contrast_low,
+              ]}>
+              <View
+                style={[
+                  a.flex_row,
+                  a.gap_md,
+                  a.p_lg,
+                  a.rounded_sm,
+                  t.atoms.bg_contrast_25,
+                ]}>
+                <CircleInfo size="md" fill={t.palette.negative_400} />
+                <View style={[a.flex_1, a.gap_sm]}>
+                  <Text style={[a.font_bold, a.leading_snug]}>
+                    {item.message}
+                  </Text>
+                  <Text
+                    style={[
+                      a.italic,
+                      a.leading_snug,
+                      t.atoms.text_contrast_medium,
+                    ]}>
+                    {item.error}
+                  </Text>
+                </View>
+              </View>
+            </View>
+          )
+        }
+        // feed previews
+        case 'preview:empty': {
+          return null // what should we do here?
+        }
+        case 'preview:loading': {
+          return (
+            <View style={[a.py_2xl, a.flex_1, a.align_center]}>
+              <Loader size="lg" />
+            </View>
+          )
+        }
+        case 'preview:header': {
+          return (
+            <ModuleHeader.Container
+              headerHeight={headerHeight}
+              style={[a.pt_xs, a.border_b, t.atoms.border_contrast_low]}>
+              <ModuleHeader.FeedLink feed={item.feed}>
+                <ModuleHeader.FeedAvatar feed={item.feed} />
+                <View style={[a.flex_1, a.gap_xs]}>
+                  <ModuleHeader.TitleText style={[a.text_lg]}>
+                    {item.feed.displayName}
+                  </ModuleHeader.TitleText>
+                  <ModuleHeader.SubtitleText>
+                    <Trans>
+                      By {sanitizeHandle(item.feed.creator.handle, '@')}
+                    </Trans>
+                  </ModuleHeader.SubtitleText>
+                </View>
+              </ModuleHeader.FeedLink>
+              <ModuleHeader.PinButton feed={item.feed} />
+            </ModuleHeader.Container>
+          )
+        }
+        case 'preview:footer': {
+          return <View style={[a.w_full, a.pt_2xl]} />
+        }
+        case 'preview:sliceItem': {
+          const slice = item.slice
+          const indexInSlice = item.indexInSlice
+          const subItem = slice.items[indexInSlice]
+          return (
+            <PostFeedItem
+              post={subItem.post}
+              record={subItem.record}
+              reason={indexInSlice === 0 ? slice.reason : undefined}
+              feedContext={slice.feedContext}
+              moderation={subItem.moderation}
+              parentAuthor={subItem.parentAuthor}
+              showReplyTo={item.showReplyTo}
+              isThreadParent={isThreadParentAt(slice.items, indexInSlice)}
+              isThreadChild={isThreadChildAt(slice.items, indexInSlice)}
+              isThreadLastChild={
+                isThreadChildAt(slice.items, indexInSlice) &&
+                slice.items.length === indexInSlice + 1
+              }
+              isParentBlocked={subItem.isParentBlocked}
+              isParentNotFound={subItem.isParentNotFound}
+              hideTopBorder={item.hideTopBorder}
+              rootPost={slice.items[0].post}
+            />
+          )
+        }
+        case 'preview:sliceViewFullThread': {
+          return <ViewFullThread uri={item.uri} />
+        }
+        case 'preview:loadMoreError': {
+          return (
+            <LoadMoreRetryBtn
+              label={_(
+                msg`There was an issue fetching posts. Tap here to try again.`,
+              )}
+              onPress={fetchNextPageFeedPreviews}
+            />
+          )
+        }
+      }
+    },
+    [
+      t,
+      focusSearchInput,
+      moderationOpts,
+      selectedInterest,
+      _,
+      fetchNextPageFeedPreviews,
+      headerHeight,
+    ],
+  )
+
+  const stickyHeaderIndices = useMemo(
+    () =>
+      items.reduce(
+        (acc, curr) =>
+          ['topBorder', 'preview:header'].includes(curr.type)
+            ? acc.concat(items.indexOf(curr))
+            : acc,
+        [] as number[],
+      ),
+    [items],
+  )
+
+  // track headers and report module viewability
+  const alreadyReportedRef = useRef<Map<string, string>>(new Map())
+  const onViewableItemsChanged = useCallback(
+    ({
+      viewableItems,
+    }: {
+      viewableItems: ViewToken<ExploreScreenItems>[]
+      changed: ViewToken<ExploreScreenItems>[]
+    }) => {
+      for (const {item} of viewableItems.filter(vi => vi.isViewable)) {
+        let module: MetricEvents['explore:module:seen']['module']
+        if (item.type === 'trendingTopics' || item.type === 'trendingVideos') {
+          module = item.type
+        } else if (item.type === 'profile') {
+          module = 'suggestedAccounts'
+        } else if (item.type === 'feed') {
+          module = 'suggestedFeeds'
+        } else if (item.type === 'preview:header') {
+          module = `feed:feedgen|${item.feed.uri}`
+        } else {
+          continue
+        }
+        if (!alreadyReportedRef.current.has(module)) {
+          alreadyReportedRef.current.set(module, module)
+          logger.metric('explore:module:seen', {module})
+        }
+      }
+    },
+    [],
+  )
+
+  return (
+    <List
+      data={items}
+      renderItem={renderItem}
+      keyExtractor={item => item.key}
+      desktopFixedHeight
+      contentContainerStyle={{paddingBottom: 100}}
+      keyboardShouldPersistTaps="handled"
+      keyboardDismissMode="on-drag"
+      stickyHeaderIndices={native(stickyHeaderIndices)}
+      viewabilityConfig={viewabilityConfig}
+      onViewableItemsChanged={onViewableItemsChanged}
+      onEndReached={onLoadMoreFeedPreviews}
+      onEndReachedThreshold={2}
+    />
+  )
+}
+
+const viewabilityConfig: ViewabilityConfig = {
+  itemVisiblePercentThreshold: 100,
+}
diff --git a/src/screens/Search/SearchResults.tsx b/src/screens/Search/SearchResults.tsx
new file mode 100644
index 000000000..bb51d2deb
--- /dev/null
+++ b/src/screens/Search/SearchResults.tsx
@@ -0,0 +1,338 @@
+import {memo, useCallback, useMemo, useState} from 'react'
+import {ActivityIndicator, View} from 'react-native'
+import {type AppBskyFeedDefs} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {augmentSearchQuery} from '#/lib/strings/helpers'
+import {useActorSearch} from '#/state/queries/actor-search'
+import {usePopularFeedsSearch} from '#/state/queries/feed'
+import {useSearchPostsQuery} from '#/state/queries/search-posts'
+import {useSession} from '#/state/session'
+import {Pager} from '#/view/com/pager/Pager'
+import {TabBar} from '#/view/com/pager/TabBar'
+import {Post} from '#/view/com/post/Post'
+import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
+import {List} from '#/view/com/util/List'
+import {atoms as a, useTheme, web} from '#/alf'
+import * as FeedCard from '#/components/FeedCard'
+import * as Layout from '#/components/Layout'
+import {Text} from '#/components/Typography'
+
+let SearchResults = ({
+  query,
+  queryWithParams,
+  activeTab,
+  onPageSelected,
+  headerHeight,
+}: {
+  query: string
+  queryWithParams: string
+  activeTab: number
+  onPageSelected: (page: number) => void
+  headerHeight: number
+}): React.ReactNode => {
+  const {_} = useLingui()
+
+  const sections = useMemo(() => {
+    if (!queryWithParams) return []
+    const noParams = queryWithParams === query
+    return [
+      {
+        title: _(msg`Top`),
+        component: (
+          <SearchScreenPostResults
+            query={queryWithParams}
+            sort="top"
+            active={activeTab === 0}
+          />
+        ),
+      },
+      {
+        title: _(msg`Latest`),
+        component: (
+          <SearchScreenPostResults
+            query={queryWithParams}
+            sort="latest"
+            active={activeTab === 1}
+          />
+        ),
+      },
+      noParams && {
+        title: _(msg`People`),
+        component: (
+          <SearchScreenUserResults query={query} active={activeTab === 2} />
+        ),
+      },
+      noParams && {
+        title: _(msg`Feeds`),
+        component: (
+          <SearchScreenFeedsResults query={query} active={activeTab === 3} />
+        ),
+      },
+    ].filter(Boolean) as {
+      title: string
+      component: React.ReactNode
+    }[]
+  }, [_, query, queryWithParams, activeTab])
+
+  return (
+    <Pager
+      onPageSelected={onPageSelected}
+      renderTabBar={props => (
+        <Layout.Center style={[a.z_10, web([a.sticky, {top: headerHeight}])]}>
+          <TabBar items={sections.map(section => section.title)} {...props} />
+        </Layout.Center>
+      )}
+      initialPage={0}>
+      {sections.map((section, i) => (
+        <View key={i}>{section.component}</View>
+      ))}
+    </Pager>
+  )
+}
+SearchResults = memo(SearchResults)
+export {SearchResults}
+
+function Loader() {
+  return (
+    <Layout.Content>
+      <View style={[a.py_xl]}>
+        <ActivityIndicator />
+      </View>
+    </Layout.Content>
+  )
+}
+
+function EmptyState({message, error}: {message: string; error?: string}) {
+  const t = useTheme()
+
+  return (
+    <Layout.Content>
+      <View style={[a.p_xl]}>
+        <View style={[t.atoms.bg_contrast_25, a.rounded_sm, a.p_lg]}>
+          <Text style={[a.text_md]}>{message}</Text>
+
+          {error && (
+            <>
+              <View
+                style={[
+                  {
+                    marginVertical: 12,
+                    height: 1,
+                    width: '100%',
+                    backgroundColor: t.atoms.text.color,
+                    opacity: 0.2,
+                  },
+                ]}
+              />
+
+              <Text style={[t.atoms.text_contrast_medium]}>
+                <Trans>Error: {error}</Trans>
+              </Text>
+            </>
+          )}
+        </View>
+      </View>
+    </Layout.Content>
+  )
+}
+
+type SearchResultSlice =
+  | {
+      type: 'post'
+      key: string
+      post: AppBskyFeedDefs.PostView
+    }
+  | {
+      type: 'loadingMore'
+      key: string
+    }
+
+let SearchScreenPostResults = ({
+  query,
+  sort,
+  active,
+}: {
+  query: string
+  sort?: 'top' | 'latest'
+  active: boolean
+}): React.ReactNode => {
+  const {_} = useLingui()
+  const {currentAccount} = useSession()
+  const [isPTR, setIsPTR] = useState(false)
+
+  const augmentedQuery = useMemo(() => {
+    return augmentSearchQuery(query || '', {did: currentAccount?.did})
+  }, [query, currentAccount])
+
+  const {
+    isFetched,
+    data: results,
+    isFetching,
+    error,
+    refetch,
+    fetchNextPage,
+    isFetchingNextPage,
+    hasNextPage,
+  } = useSearchPostsQuery({query: augmentedQuery, sort, enabled: active})
+
+  const onPullToRefresh = useCallback(async () => {
+    setIsPTR(true)
+    await refetch()
+    setIsPTR(false)
+  }, [setIsPTR, refetch])
+  const onEndReached = useCallback(() => {
+    if (isFetching || !hasNextPage || error) return
+    fetchNextPage()
+  }, [isFetching, error, hasNextPage, fetchNextPage])
+
+  const posts = useMemo(() => {
+    return results?.pages.flatMap(page => page.posts) || []
+  }, [results])
+  const items = useMemo(() => {
+    let temp: SearchResultSlice[] = []
+
+    const seenUris = new Set()
+    for (const post of posts) {
+      if (seenUris.has(post.uri)) {
+        continue
+      }
+      temp.push({
+        type: 'post',
+        key: post.uri,
+        post,
+      })
+      seenUris.add(post.uri)
+    }
+
+    if (isFetchingNextPage) {
+      temp.push({
+        type: 'loadingMore',
+        key: 'loadingMore',
+      })
+    }
+
+    return temp
+  }, [posts, isFetchingNextPage])
+
+  return error ? (
+    <EmptyState
+      message={_(
+        msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`,
+      )}
+      error={error.toString()}
+    />
+  ) : (
+    <>
+      {isFetched ? (
+        <>
+          {posts.length ? (
+            <List
+              data={items}
+              renderItem={({item}) => {
+                if (item.type === 'post') {
+                  return <Post post={item.post} />
+                } else {
+                  return null
+                }
+              }}
+              keyExtractor={item => item.key}
+              refreshing={isPTR}
+              onRefresh={onPullToRefresh}
+              onEndReached={onEndReached}
+              desktopFixedHeight
+              contentContainerStyle={{paddingBottom: 100}}
+            />
+          ) : (
+            <EmptyState message={_(msg`No results found for ${query}`)} />
+          )}
+        </>
+      ) : (
+        <Loader />
+      )}
+    </>
+  )
+}
+SearchScreenPostResults = memo(SearchScreenPostResults)
+
+let SearchScreenUserResults = ({
+  query,
+  active,
+}: {
+  query: string
+  active: boolean
+}): React.ReactNode => {
+  const {_} = useLingui()
+
+  const {data: results, isFetched} = useActorSearch({
+    query,
+    enabled: active,
+  })
+
+  return isFetched && results ? (
+    <>
+      {results.length ? (
+        <List
+          data={results}
+          renderItem={({item}) => (
+            <ProfileCardWithFollowBtn profile={item} noBg />
+          )}
+          keyExtractor={item => item.did}
+          desktopFixedHeight
+          contentContainerStyle={{paddingBottom: 100}}
+        />
+      ) : (
+        <EmptyState message={_(msg`No results found for ${query}`)} />
+      )}
+    </>
+  ) : (
+    <Loader />
+  )
+}
+SearchScreenUserResults = memo(SearchScreenUserResults)
+
+let SearchScreenFeedsResults = ({
+  query,
+  active,
+}: {
+  query: string
+  active: boolean
+}): React.ReactNode => {
+  const t = useTheme()
+  const {_} = useLingui()
+
+  const {data: results, isFetched} = usePopularFeedsSearch({
+    query,
+    enabled: active,
+  })
+
+  return isFetched && results ? (
+    <>
+      {results.length ? (
+        <List
+          data={results}
+          renderItem={({item}) => (
+            <View
+              style={[
+                a.border_b,
+                t.atoms.border_contrast_low,
+                a.px_lg,
+                a.py_lg,
+              ]}>
+              <FeedCard.Default view={item} />
+            </View>
+          )}
+          keyExtractor={item => item.uri}
+          desktopFixedHeight
+          contentContainerStyle={{paddingBottom: 100}}
+        />
+      ) : (
+        <EmptyState message={_(msg`No results found for ${query}`)} />
+      )}
+    </>
+  ) : (
+    <Loader />
+  )
+}
+SearchScreenFeedsResults = memo(SearchScreenFeedsResults)
diff --git a/src/screens/Search/Shell.tsx b/src/screens/Search/Shell.tsx
new file mode 100644
index 000000000..e930b8289
--- /dev/null
+++ b/src/screens/Search/Shell.tsx
@@ -0,0 +1,535 @@
+import {
+  memo,
+  useCallback,
+  useLayoutEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'react'
+import {
+  type StyleProp,
+  type TextInput,
+  View,
+  type ViewStyle,
+} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {HITSLOP_20} from '#/lib/constants'
+import {HITSLOP_10} from '#/lib/constants'
+import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+import {MagnifyingGlassIcon} from '#/lib/icons'
+import {type NavigationProp} from '#/lib/routes/types'
+import {isWeb} from '#/platform/detection'
+import {listenSoftReset} from '#/state/events'
+import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
+import {
+  unstableCacheProfileView,
+  useProfilesQuery,
+} from '#/state/queries/profile'
+import {useSession} from '#/state/session'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {
+  makeSearchQuery,
+  type Params,
+  parseSearchQuery,
+} from '#/screens/Search/utils'
+import {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import {SearchInput} from '#/components/forms/SearchInput'
+import * as Layout from '#/components/Layout'
+import {Text} from '#/components/Typography'
+import {account, useStorage} from '#/storage'
+import type * as bsky from '#/types/bsky'
+import {AutocompleteResults} from './components/AutocompleteResults'
+import {SearchHistory} from './components/SearchHistory'
+import {SearchLanguageDropdown} from './components/SearchLanguageDropdown'
+import {Explore} from './Explore'
+import {SearchResults} from './SearchResults'
+
+export function SearchScreenShell({
+  queryParam,
+  testID,
+  fixedParams,
+  navButton = 'menu',
+  inputPlaceholder,
+}: {
+  queryParam: string
+  testID: string
+  fixedParams?: Params
+  navButton?: 'back' | 'menu'
+  inputPlaceholder?: string
+}) {
+  const t = useTheme()
+  const {gtMobile} = useBreakpoints()
+  const navigation = useNavigation<NavigationProp>()
+  const route = useRoute()
+  const textInput = useRef<TextInput>(null)
+  const {_} = useLingui()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {currentAccount} = useSession()
+  const queryClient = useQueryClient()
+
+  // Query terms
+  const [searchText, setSearchText] = useState<string>(queryParam)
+  const {data: autocompleteData, isFetching: isAutocompleteFetching} =
+    useActorAutocompleteQuery(searchText, true)
+
+  const [showAutocomplete, setShowAutocomplete] = useState(false)
+
+  const [termHistory = [], setTermHistory] = useStorage(account, [
+    currentAccount?.did ?? 'pwi',
+    'searchTermHistory',
+  ] as const)
+  const [accountHistory = [], setAccountHistory] = useStorage(account, [
+    currentAccount?.did ?? 'pwi',
+    'searchAccountHistory',
+  ])
+
+  const {data: accountHistoryProfiles} = useProfilesQuery({
+    handles: accountHistory,
+    maintainData: true,
+  })
+
+  const updateSearchHistory = useCallback(
+    async (item: string) => {
+      if (!item) return
+      const newSearchHistory = [
+        item,
+        ...termHistory.filter(search => search !== item),
+      ].slice(0, 6)
+      setTermHistory(newSearchHistory)
+    },
+    [termHistory, setTermHistory],
+  )
+
+  const updateProfileHistory = useCallback(
+    async (item: bsky.profile.AnyProfileView) => {
+      const newAccountHistory = [
+        item.did,
+        ...accountHistory.filter(p => p !== item.did),
+      ].slice(0, 5)
+      setAccountHistory(newAccountHistory)
+    },
+    [accountHistory, setAccountHistory],
+  )
+
+  const deleteSearchHistoryItem = useCallback(
+    async (item: string) => {
+      setTermHistory(termHistory.filter(search => search !== item))
+    },
+    [termHistory, setTermHistory],
+  )
+  const deleteProfileHistoryItem = useCallback(
+    async (item: bsky.profile.AnyProfileView) => {
+      setAccountHistory(accountHistory.filter(p => p !== item.did))
+    },
+    [accountHistory, setAccountHistory],
+  )
+
+  const {params, query, queryWithParams} = useQueryManager({
+    initialQuery: queryParam,
+    fixedParams,
+  })
+  const showFilters = Boolean(queryWithParams && !showAutocomplete)
+
+  // web only - measure header height for sticky positioning
+  const [headerHeight, setHeaderHeight] = useState(0)
+  const headerRef = useRef(null)
+  useLayoutEffect(() => {
+    if (isWeb) {
+      if (!headerRef.current) return
+      const measurement = (headerRef.current as Element).getBoundingClientRect()
+      setHeaderHeight(measurement.height)
+    }
+  }, [])
+
+  useFocusEffect(
+    useNonReactiveCallback(() => {
+      if (isWeb) {
+        setSearchText(queryParam)
+      }
+    }),
+  )
+
+  const onPressClearQuery = useCallback(() => {
+    scrollToTopWeb()
+    setSearchText('')
+    textInput.current?.focus()
+  }, [])
+
+  const onChangeText = useCallback(async (text: string) => {
+    scrollToTopWeb()
+    setSearchText(text)
+  }, [])
+
+  const navigateToItem = useCallback(
+    (item: string) => {
+      scrollToTopWeb()
+      setShowAutocomplete(false)
+      updateSearchHistory(item)
+
+      if (isWeb) {
+        // @ts-expect-error route is not typesafe
+        navigation.push(route.name, {...route.params, q: item})
+      } else {
+        textInput.current?.blur()
+        navigation.setParams({q: item})
+      }
+    },
+    [updateSearchHistory, navigation, route],
+  )
+
+  const onPressCancelSearch = useCallback(() => {
+    scrollToTopWeb()
+    textInput.current?.blur()
+    setShowAutocomplete(false)
+    if (isWeb) {
+      // Empty params resets the URL to be /search rather than /search?q=
+
+      const {q: _q, ...parameters} = (route.params ?? {}) as {
+        [key: string]: string
+      }
+      // @ts-expect-error route is not typesafe
+      navigation.replace(route.name, parameters)
+    } else {
+      setSearchText('')
+      navigation.setParams({q: ''})
+    }
+  }, [setShowAutocomplete, setSearchText, navigation, route.params, route.name])
+
+  const onSubmit = useCallback(() => {
+    navigateToItem(searchText)
+  }, [navigateToItem, searchText])
+
+  const onAutocompleteResultPress = useCallback(() => {
+    if (isWeb) {
+      setShowAutocomplete(false)
+    } else {
+      textInput.current?.blur()
+    }
+  }, [])
+
+  const handleHistoryItemClick = useCallback(
+    (item: string) => {
+      setSearchText(item)
+      navigateToItem(item)
+    },
+    [navigateToItem],
+  )
+
+  const handleProfileClick = useCallback(
+    (profile: bsky.profile.AnyProfileView) => {
+      unstableCacheProfileView(queryClient, profile)
+      // Slight delay to avoid updating during push nav animation.
+      setTimeout(() => {
+        updateProfileHistory(profile)
+      }, 400)
+    },
+    [updateProfileHistory, queryClient],
+  )
+
+  const onSoftReset = useCallback(() => {
+    if (isWeb) {
+      // Empty params resets the URL to be /search rather than /search?q=
+
+      const {q: _q, ...parameters} = (route.params ?? {}) as {
+        [key: string]: string
+      }
+      // @ts-expect-error route is not typesafe
+      navigation.replace(route.name, parameters)
+    } else {
+      setSearchText('')
+      navigation.setParams({q: ''})
+      textInput.current?.focus()
+    }
+  }, [navigation, route])
+
+  useFocusEffect(
+    useCallback(() => {
+      setMinimalShellMode(false)
+      return listenSoftReset(onSoftReset)
+    }, [onSoftReset, setMinimalShellMode]),
+  )
+
+  const onSearchInputFocus = useCallback(() => {
+    if (isWeb) {
+      // Prevent a jump on iPad by ensuring that
+      // the initial focused render has no result list.
+      requestAnimationFrame(() => {
+        setShowAutocomplete(true)
+      })
+    } else {
+      setShowAutocomplete(true)
+    }
+  }, [setShowAutocomplete])
+
+  const focusSearchInput = useCallback(() => {
+    textInput.current?.focus()
+  }, [])
+
+  const showHeader = !gtMobile || navButton !== 'menu'
+
+  return (
+    <Layout.Screen testID={testID}>
+      <View
+        ref={headerRef}
+        onLayout={evt => {
+          if (isWeb) setHeaderHeight(evt.nativeEvent.layout.height)
+        }}
+        style={[
+          a.relative,
+          a.z_10,
+          web({
+            position: 'sticky',
+            top: 0,
+          }),
+        ]}>
+        <Layout.Center style={t.atoms.bg}>
+          {showHeader && (
+            <View
+              // HACK: shift up search input. we can't remove the top padding
+              // on the search input because it messes up the layout animation
+              // if we add it only when the header is hidden
+              style={{marginBottom: tokens.space.xs * -1}}>
+              <Layout.Header.Outer noBottomBorder>
+                {navButton === 'menu' ? (
+                  <Layout.Header.MenuButton />
+                ) : (
+                  <Layout.Header.BackButton />
+                )}
+                <Layout.Header.Content align="left">
+                  <Layout.Header.TitleText>
+                    <Trans>Search</Trans>
+                  </Layout.Header.TitleText>
+                </Layout.Header.Content>
+                {showFilters ? (
+                  <SearchLanguageDropdown
+                    value={params.lang}
+                    onChange={params.setLang}
+                  />
+                ) : (
+                  <Layout.Header.Slot />
+                )}
+              </Layout.Header.Outer>
+            </View>
+          )}
+          <View style={[a.px_md, a.pt_sm, a.pb_sm, a.overflow_hidden]}>
+            <View style={[a.gap_sm]}>
+              <View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs]}>
+                <View style={[a.flex_1]}>
+                  <SearchInput
+                    ref={textInput}
+                    value={searchText}
+                    onFocus={onSearchInputFocus}
+                    onChangeText={onChangeText}
+                    onClearText={onPressClearQuery}
+                    onSubmitEditing={onSubmit}
+                    placeholder={
+                      inputPlaceholder ??
+                      _(msg`Search for posts, users, or feeds`)
+                    }
+                    hitSlop={{...HITSLOP_20, top: 0}}
+                  />
+                </View>
+                {showAutocomplete && (
+                  <Button
+                    label={_(msg`Cancel search`)}
+                    size="large"
+                    variant="ghost"
+                    color="secondary"
+                    style={[a.px_sm]}
+                    onPress={onPressCancelSearch}
+                    hitSlop={HITSLOP_10}>
+                    <ButtonText>
+                      <Trans>Cancel</Trans>
+                    </ButtonText>
+                  </Button>
+                )}
+              </View>
+
+              {showFilters && !showHeader && (
+                <View
+                  style={[
+                    a.flex_row,
+                    a.align_center,
+                    a.justify_between,
+                    a.gap_sm,
+                  ]}>
+                  <SearchLanguageDropdown
+                    value={params.lang}
+                    onChange={params.setLang}
+                  />
+                </View>
+              )}
+            </View>
+          </View>
+        </Layout.Center>
+      </View>
+
+      <View
+        style={{
+          display: showAutocomplete && !fixedParams ? 'flex' : 'none',
+          flex: 1,
+        }}>
+        {searchText.length > 0 ? (
+          <AutocompleteResults
+            isAutocompleteFetching={isAutocompleteFetching}
+            autocompleteData={autocompleteData}
+            searchText={searchText}
+            onSubmit={onSubmit}
+            onResultPress={onAutocompleteResultPress}
+            onProfileClick={handleProfileClick}
+          />
+        ) : (
+          <SearchHistory
+            searchHistory={termHistory}
+            selectedProfiles={accountHistoryProfiles?.profiles || []}
+            onItemClick={handleHistoryItemClick}
+            onProfileClick={handleProfileClick}
+            onRemoveItemClick={deleteSearchHistoryItem}
+            onRemoveProfileClick={deleteProfileHistoryItem}
+          />
+        )}
+      </View>
+      <View
+        style={{
+          display: showAutocomplete ? 'none' : 'flex',
+          flex: 1,
+        }}>
+        <SearchScreenInner
+          query={query}
+          queryWithParams={queryWithParams}
+          headerHeight={headerHeight}
+          focusSearchInput={focusSearchInput}
+        />
+      </View>
+    </Layout.Screen>
+  )
+}
+
+let SearchScreenInner = ({
+  query,
+  queryWithParams,
+  headerHeight,
+  focusSearchInput,
+}: {
+  query: string
+  queryWithParams: string
+  headerHeight: number
+  focusSearchInput: () => void
+}): React.ReactNode => {
+  const t = useTheme()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {hasSession} = useSession()
+  const {gtTablet} = useBreakpoints()
+  const [activeTab, setActiveTab] = useState(0)
+  const {_} = useLingui()
+
+  const onPageSelected = useCallback(
+    (index: number) => {
+      setMinimalShellMode(false)
+      setActiveTab(index)
+    },
+    [setMinimalShellMode],
+  )
+
+  return queryWithParams ? (
+    <SearchResults
+      query={query}
+      queryWithParams={queryWithParams}
+      activeTab={activeTab}
+      headerHeight={headerHeight}
+      onPageSelected={onPageSelected}
+    />
+  ) : hasSession ? (
+    <Explore focusSearchInput={focusSearchInput} headerHeight={headerHeight} />
+  ) : (
+    <Layout.Center>
+      <View style={a.flex_1}>
+        {gtTablet && (
+          <View
+            style={[
+              a.border_b,
+              t.atoms.border_contrast_low,
+              a.px_lg,
+              a.pt_sm,
+              a.pb_lg,
+            ]}>
+            <Text style={[a.text_2xl, a.font_heavy]}>
+              <Trans>Search</Trans>
+            </Text>
+          </View>
+        )}
+
+        <View style={[a.align_center, a.justify_center, a.py_4xl, a.gap_lg]}>
+          <MagnifyingGlassIcon
+            strokeWidth={3}
+            size={60}
+            style={t.atoms.text_contrast_medium as StyleProp<ViewStyle>}
+          />
+          <Text style={[t.atoms.text_contrast_medium, a.text_md]}>
+            <Trans>Find posts, users, and feeds on Bluesky</Trans>
+          </Text>
+        </View>
+      </View>
+    </Layout.Center>
+  )
+}
+SearchScreenInner = memo(SearchScreenInner)
+
+function useQueryManager({
+  initialQuery,
+  fixedParams,
+}: {
+  initialQuery: string
+  fixedParams?: Params
+}) {
+  const {query, params: initialParams} = useMemo(() => {
+    return parseSearchQuery(initialQuery || '')
+  }, [initialQuery])
+  const [prevInitialQuery, setPrevInitialQuery] = useState(initialQuery)
+  const [lang, setLang] = useState(initialParams.lang || '')
+
+  if (initialQuery !== prevInitialQuery) {
+    // handle new queryParam change (from manual search entry)
+    setPrevInitialQuery(initialQuery)
+    setLang(initialParams.lang || '')
+  }
+
+  const params = useMemo(
+    () => ({
+      // default stuff
+      ...initialParams,
+      // managed stuff
+      lang,
+      ...fixedParams,
+    }),
+    [lang, initialParams, fixedParams],
+  )
+  const handlers = useMemo(
+    () => ({
+      setLang,
+    }),
+    [setLang],
+  )
+
+  return useMemo(() => {
+    return {
+      query,
+      queryWithParams: makeSearchQuery(query, params),
+      params: {
+        ...params,
+        ...handlers,
+      },
+    }
+  }, [query, params, handlers])
+}
+
+function scrollToTopWeb() {
+  if (isWeb) {
+    window.scrollTo(0, 0)
+  }
+}
diff --git a/src/screens/Search/components/AutocompleteResults.tsx b/src/screens/Search/components/AutocompleteResults.tsx
new file mode 100644
index 000000000..58a0dec77
--- /dev/null
+++ b/src/screens/Search/components/AutocompleteResults.tsx
@@ -0,0 +1,71 @@
+import {memo} from 'react'
+import {ActivityIndicator, View} from 'react-native'
+import {type AppBskyActorDefs, moderateProfile} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {isNative} from '#/platform/detection'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
+import {atoms as a, native} from '#/alf'
+import * as Layout from '#/components/Layout'
+
+let AutocompleteResults = ({
+  isAutocompleteFetching,
+  autocompleteData,
+  searchText,
+  onSubmit,
+  onResultPress,
+  onProfileClick,
+}: {
+  isAutocompleteFetching: boolean
+  autocompleteData: AppBskyActorDefs.ProfileViewBasic[] | undefined
+  searchText: string
+  onSubmit: () => void
+  onResultPress: () => void
+  onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
+}): React.ReactNode => {
+  const moderationOpts = useModerationOpts()
+  const {_} = useLingui()
+  return (
+    <>
+      {(isAutocompleteFetching && !autocompleteData?.length) ||
+      !moderationOpts ? (
+        <Layout.Content>
+          <View style={[a.py_xl]}>
+            <ActivityIndicator />
+          </View>
+        </Layout.Content>
+      ) : (
+        <Layout.Content
+          keyboardShouldPersistTaps="handled"
+          keyboardDismissMode="on-drag">
+          <SearchLinkCard
+            label={_(msg`Search for "${searchText}"`)}
+            onPress={native(onSubmit)}
+            to={
+              isNative
+                ? undefined
+                : `/search?q=${encodeURIComponent(searchText)}`
+            }
+            style={{borderBottomWidth: 1}}
+          />
+          {autocompleteData?.map(item => (
+            <SearchProfileCard
+              key={item.did}
+              profile={item}
+              moderation={moderateProfile(item, moderationOpts)}
+              onPress={() => {
+                onProfileClick(item)
+                onResultPress()
+              }}
+            />
+          ))}
+          <View style={{height: 200}} />
+        </Layout.Content>
+      )}
+    </>
+  )
+}
+AutocompleteResults = memo(AutocompleteResults)
+export {AutocompleteResults}
diff --git a/src/screens/Search/components/ExploreTrendingTopics.tsx b/src/screens/Search/components/ExploreTrendingTopics.tsx
deleted file mode 100644
index a010ad8dc..000000000
--- a/src/screens/Search/components/ExploreTrendingTopics.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {logEvent} from '#/lib/statsig/statsig'
-import {isWeb} from '#/platform/detection'
-import {
-  useTrendingSettings,
-  useTrendingSettingsApi,
-} from '#/state/preferences/trending'
-import {
-  DEFAULT_LIMIT as TRENDING_TOPICS_COUNT,
-  useTrendingTopics,
-} from '#/state/queries/trending/useTrendingTopics'
-import {useTrendingConfig} from '#/state/trending-config'
-import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
-import {Button, ButtonIcon} from '#/components/Button'
-import {GradientFill} from '#/components/GradientFill'
-import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
-import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2'
-import * as Prompt from '#/components/Prompt'
-import {
-  TrendingTopic,
-  TrendingTopicLink,
-  TrendingTopicSkeleton,
-} from '#/components/TrendingTopics'
-import {Text} from '#/components/Typography'
-
-export function ExploreTrendingTopics() {
-  const {enabled} = useTrendingConfig()
-  const {trendingDisabled} = useTrendingSettings()
-  return enabled && !trendingDisabled ? <Inner /> : null
-}
-
-function Inner() {
-  const t = useTheme()
-  const {_} = useLingui()
-  const gutters = useGutters([0, 'compact'])
-  const {data: trending, error, isLoading} = useTrendingTopics()
-  const noTopics = !isLoading && !error && !trending?.topics?.length
-  const {setTrendingDisabled} = useTrendingSettingsApi()
-  const trendingPrompt = Prompt.usePromptControl()
-
-  const onConfirmHide = React.useCallback(() => {
-    logEvent('trendingTopics:hide', {context: 'explore:trending'})
-    setTrendingDisabled(true)
-  }, [setTrendingDisabled])
-
-  return error || noTopics ? null : (
-    <>
-      <View
-        style={[
-          a.flex_row,
-          isWeb
-            ? [a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
-            : [a.p_lg, a.pt_2xl, a.gap_md],
-          a.border_b,
-          t.atoms.border_contrast_low,
-        ]}>
-        <View style={[a.flex_1, a.gap_sm]}>
-          <View style={[a.flex_row, a.align_center, a.gap_sm]}>
-            <Trending
-              size="lg"
-              fill={t.palette.primary_500}
-              style={{marginLeft: -2}}
-            />
-            <Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>
-              <Trans>Trending</Trans>
-            </Text>
-            <View style={[a.py_xs, a.px_sm, a.rounded_sm, a.overflow_hidden]}>
-              <GradientFill gradient={tokens.gradients.primary} />
-              <Text style={[a.text_sm, a.font_heavy, {color: 'white'}]}>
-                <Trans>BETA</Trans>
-              </Text>
-            </View>
-          </View>
-          <Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
-            <Trans>What people are posting about.</Trans>
-          </Text>
-        </View>
-        <Button
-          label={_(msg`Hide trending topics`)}
-          size="small"
-          variant="ghost"
-          color="secondary"
-          shape="round"
-          onPress={() => trendingPrompt.open()}>
-          <ButtonIcon icon={X} />
-        </Button>
-      </View>
-
-      <View style={[a.pt_md, a.pb_lg]}>
-        <View
-          style={[
-            a.flex_row,
-            a.justify_start,
-            a.flex_wrap,
-            {rowGap: 8, columnGap: 6},
-            gutters,
-          ]}>
-          {isLoading ? (
-            Array(TRENDING_TOPICS_COUNT)
-              .fill(0)
-              .map((_, i) => <TrendingTopicSkeleton key={i} index={i} />)
-          ) : !trending?.topics ? null : (
-            <>
-              {trending.topics.map(topic => (
-                <TrendingTopicLink
-                  key={topic.link}
-                  topic={topic}
-                  onPress={() => {
-                    logEvent('trendingTopic:click', {context: 'explore'})
-                  }}>
-                  {({hovered}) => (
-                    <TrendingTopic
-                      topic={topic}
-                      style={[
-                        hovered && [
-                          t.atoms.border_contrast_high,
-                          t.atoms.bg_contrast_25,
-                        ],
-                      ]}
-                    />
-                  )}
-                </TrendingTopicLink>
-              ))}
-            </>
-          )}
-        </View>
-      </View>
-
-      <Prompt.Basic
-        control={trendingPrompt}
-        title={_(msg`Hide trending topics?`)}
-        description={_(msg`You can update this later from your settings.`)}
-        confirmButtonCta={_(msg`Hide`)}
-        onConfirm={onConfirmHide}
-      />
-    </>
-  )
-}
diff --git a/src/screens/Search/components/ModuleHeader.tsx b/src/screens/Search/components/ModuleHeader.tsx
new file mode 100644
index 000000000..cbd0a856b
--- /dev/null
+++ b/src/screens/Search/components/ModuleHeader.tsx
@@ -0,0 +1,170 @@
+import {useMemo} from 'react'
+import {View} from 'react-native'
+import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
+
+import {PressableScale} from '#/lib/custom-animations/PressableScale'
+import {makeCustomFeedLink} from '#/lib/routes/links'
+import {logger} from '#/logger'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {
+  atoms as a,
+  native,
+  useGutters,
+  useTheme,
+  type ViewStyleProp,
+  web,
+} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
+import * as FeedCard from '#/components/FeedCard'
+import {sizes as iconSizes} from '#/components/icons/common'
+import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2'
+import {Link} from '#/components/Link'
+import {Text, type TextProps} from '#/components/Typography'
+
+export function Container({
+  style,
+  children,
+  headerHeight,
+}: {children: React.ReactNode; headerHeight?: number} & ViewStyleProp) {
+  const t = useTheme()
+  const gutters = useGutters([0, 'base'])
+  return (
+    <View
+      style={[
+        gutters,
+        a.flex_row,
+        a.align_center,
+        a.pt_2xl,
+        a.pb_md,
+        a.gap_sm,
+        t.atoms.bg,
+        headerHeight && web({position: 'sticky', top: headerHeight}),
+        style,
+      ]}>
+      {children}
+    </View>
+  )
+}
+
+export function FeedLink({
+  feed,
+  children,
+}: {
+  feed: AppBskyFeedDefs.GeneratorView
+  children?: React.ReactNode
+}) {
+  const t = useTheme()
+  const {host: did, rkey} = useMemo(() => new AtUri(feed.uri), [feed.uri])
+  return (
+    <Link
+      to={makeCustomFeedLink(did, rkey)}
+      label={feed.displayName}
+      style={[a.flex_1]}>
+      {({focused, hovered, pressed}) => (
+        <View
+          style={[
+            a.flex_1,
+            a.flex_row,
+            a.align_center,
+            {gap: 10},
+            a.rounded_md,
+            a.p_xs,
+            {marginLeft: -6},
+            (focused || hovered || pressed) && t.atoms.bg_contrast_25,
+          ]}>
+          {children}
+        </View>
+      )}
+    </Link>
+  )
+}
+
+export function FeedAvatar({feed}: {feed: AppBskyFeedDefs.GeneratorView}) {
+  return <UserAvatar type="algo" size={38} avatar={feed.avatar} />
+}
+
+export function Icon({
+  icon: Comp,
+  size = 'lg',
+}: Pick<React.ComponentProps<typeof ButtonIcon>, 'icon' | 'size'>) {
+  const iconSize = iconSizes[size]
+
+  return (
+    <View style={[a.z_20, {width: iconSize, height: iconSize, marginLeft: -2}]}>
+      <Comp width={iconSize} />
+    </View>
+  )
+}
+
+export function TitleText({style, ...props}: TextProps) {
+  return (
+    <Text style={[a.font_bold, a.flex_1, a.text_xl, style]} emoji {...props} />
+  )
+}
+
+export function SubtitleText({style, ...props}: TextProps) {
+  const t = useTheme()
+  return (
+    <Text
+      style={[
+        t.atoms.text_contrast_medium,
+        a.leading_tight,
+        a.flex_1,
+        a.text_sm,
+        style,
+      ]}
+      {...props}
+    />
+  )
+}
+
+export function SearchButton({
+  label,
+  metricsTag,
+  onPress,
+}: {
+  label: string
+  metricsTag: 'suggestedAccounts' | 'suggestedFeeds'
+  onPress?: () => void
+}) {
+  return (
+    <Button
+      label={label}
+      size="small"
+      variant="ghost"
+      color="secondary"
+      shape="round"
+      PressableComponent={native(PressableScale)}
+      onPress={() => {
+        logger.metric(
+          'explore:module:searchButtonPress',
+          {module: metricsTag},
+          {statsig: true},
+        )
+        onPress?.()
+      }}
+      style={[
+        {
+          right: -4,
+        },
+      ]}>
+      <ButtonIcon icon={SearchIcon} size="lg" />
+    </Button>
+  )
+}
+
+export function PinButton({feed}: {feed: AppBskyFeedDefs.GeneratorView}) {
+  return (
+    <View style={[a.z_20, {marginRight: -6}]}>
+      <FeedCard.SaveButton
+        pin
+        view={feed}
+        size="large"
+        color="secondary"
+        variant="ghost"
+        shape="square"
+        text={false}
+      />
+    </View>
+  )
+}
diff --git a/src/screens/Search/components/SearchHistory.tsx b/src/screens/Search/components/SearchHistory.tsx
new file mode 100644
index 000000000..5e62f2cd0
--- /dev/null
+++ b/src/screens/Search/components/SearchHistory.tsx
@@ -0,0 +1,169 @@
+import {Pressable, ScrollView, StyleSheet, View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {createHitslop, HITSLOP_10} from '#/lib/constants'
+import {makeProfileLink} from '#/lib/routes/links'
+import {sanitizeDisplayName} from '#/lib/strings/display-names'
+import {Link} from '#/view/com/util/Link'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
+import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
+import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
+import * as Layout from '#/components/Layout'
+import {Text} from '#/components/Typography'
+import type * as bsky from '#/types/bsky'
+
+export function SearchHistory({
+  searchHistory,
+  selectedProfiles,
+  onItemClick,
+  onProfileClick,
+  onRemoveItemClick,
+  onRemoveProfileClick,
+}: {
+  searchHistory: string[]
+  selectedProfiles: bsky.profile.AnyProfileView[]
+  onItemClick: (item: string) => void
+  onProfileClick: (profile: bsky.profile.AnyProfileView) => void
+  onRemoveItemClick: (item: string) => void
+  onRemoveProfileClick: (profile: bsky.profile.AnyProfileView) => void
+}) {
+  const {gtMobile} = useBreakpoints()
+  const t = useTheme()
+  const {_} = useLingui()
+
+  return (
+    <Layout.Content
+      keyboardDismissMode="interactive"
+      keyboardShouldPersistTaps="handled">
+      <View style={[a.w_full, a.px_md]}>
+        {(searchHistory.length > 0 || selectedProfiles.length > 0) && (
+          <Text style={[a.text_md, a.font_bold, a.p_md]}>
+            <Trans>Recent Searches</Trans>
+          </Text>
+        )}
+        {selectedProfiles.length > 0 && (
+          <View
+            style={[
+              styles.selectedProfilesContainer,
+              !gtMobile && styles.selectedProfilesContainerMobile,
+            ]}>
+            <BlockDrawerGesture>
+              <ScrollView
+                horizontal
+                keyboardShouldPersistTaps="handled"
+                style={[
+                  a.flex_row,
+                  a.flex_nowrap,
+                  {marginHorizontal: tokens.space._2xl * -1},
+                ]}
+                contentContainerStyle={[a.px_2xl, a.border_0]}>
+                {selectedProfiles.slice(0, 5).map((profile, index) => (
+                  <View
+                    key={index}
+                    style={[
+                      styles.profileItem,
+                      !gtMobile && styles.profileItemMobile,
+                    ]}>
+                    <Link
+                      href={makeProfileLink(profile)}
+                      title={profile.handle}
+                      asAnchor
+                      anchorNoUnderline
+                      onBeforePress={() => onProfileClick(profile)}
+                      style={[a.align_center, a.w_full]}>
+                      <UserAvatar
+                        avatar={profile.avatar}
+                        type={profile.associated?.labeler ? 'labeler' : 'user'}
+                        size={60}
+                      />
+                      <Text
+                        emoji
+                        style={[a.text_xs, a.text_center, styles.profileName]}
+                        numberOfLines={1}>
+                        {sanitizeDisplayName(
+                          profile.displayName || profile.handle,
+                        )}
+                      </Text>
+                    </Link>
+                    <Pressable
+                      accessibilityRole="button"
+                      accessibilityLabel={_(msg`Remove profile`)}
+                      accessibilityHint={_(
+                        msg`Removes profile from search history`,
+                      )}
+                      onPress={() => onRemoveProfileClick(profile)}
+                      hitSlop={createHitslop(6)}
+                      style={styles.profileRemoveBtn}>
+                      <XIcon size="xs" style={t.atoms.text_contrast_low} />
+                    </Pressable>
+                  </View>
+                ))}
+              </ScrollView>
+            </BlockDrawerGesture>
+          </View>
+        )}
+        {searchHistory.length > 0 && (
+          <View style={[a.pl_md, a.pr_xs, a.mt_md]}>
+            {searchHistory.slice(0, 5).map((historyItem, index) => (
+              <View key={index} style={[a.flex_row, a.align_center, a.mt_xs]}>
+                <Pressable
+                  accessibilityRole="button"
+                  onPress={() => onItemClick(historyItem)}
+                  hitSlop={HITSLOP_10}
+                  style={[a.flex_1, a.py_md]}>
+                  <Text style={[a.text_md]}>{historyItem}</Text>
+                </Pressable>
+                <Button
+                  label={_(msg`Remove ${historyItem}`)}
+                  onPress={() => onRemoveItemClick(historyItem)}
+                  size="small"
+                  variant="ghost"
+                  color="secondary"
+                  shape="round">
+                  <ButtonIcon icon={XIcon} />
+                </Button>
+              </View>
+            ))}
+          </View>
+        )}
+      </View>
+    </Layout.Content>
+  )
+}
+
+const styles = StyleSheet.create({
+  selectedProfilesContainer: {
+    marginTop: 10,
+    paddingHorizontal: 12,
+    height: 80,
+  },
+  selectedProfilesContainerMobile: {
+    height: 100,
+  },
+  profileItem: {
+    alignItems: 'center',
+    marginRight: 15,
+    width: 78,
+  },
+  profileItemMobile: {
+    width: 70,
+  },
+  profileName: {
+    width: 78,
+    marginTop: 6,
+  },
+  profileRemoveBtn: {
+    position: 'absolute',
+    top: 0,
+    right: 5,
+    backgroundColor: 'white',
+    borderRadius: 10,
+    width: 18,
+    height: 18,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+})
diff --git a/src/screens/Search/components/SearchLanguageDropdown.tsx b/src/screens/Search/components/SearchLanguageDropdown.tsx
new file mode 100644
index 000000000..5c5a4b74f
--- /dev/null
+++ b/src/screens/Search/components/SearchLanguageDropdown.tsx
@@ -0,0 +1,120 @@
+import {useMemo} from 'react'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {languageName} from '#/locale/helpers'
+import {APP_LANGUAGES, LANGUAGES} from '#/locale/languages'
+import {useLanguagePrefs} from '#/state/preferences'
+import {atoms as a, native, platform, tokens} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {
+  ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
+  ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon,
+} from '#/components/icons/Chevron'
+import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe'
+import * as Menu from '#/components/Menu'
+
+export function SearchLanguageDropdown({
+  value,
+  onChange,
+}: {
+  value: string
+  onChange(value: string): void
+}) {
+  const {_} = useLingui()
+  const {appLanguage, contentLanguages} = useLanguagePrefs()
+
+  const languages = useMemo(() => {
+    return LANGUAGES.filter(
+      (lang, index, self) =>
+        Boolean(lang.code2) && // reduce to the code2 varieties
+        index === self.findIndex(t => t.code2 === lang.code2), // remove dupes (which will happen)
+    )
+      .map(l => ({
+        label: languageName(l, appLanguage),
+        value: l.code2,
+        key: l.code2 + l.code3,
+      }))
+      .sort((a, b) => {
+        // prioritize user's languages
+        const aIsUser = contentLanguages.includes(a.value)
+        const bIsUser = contentLanguages.includes(b.value)
+        if (aIsUser && !bIsUser) return -1
+        if (bIsUser && !aIsUser) return 1
+        // prioritize "common" langs in the network
+        const aIsCommon = !!APP_LANGUAGES.find(
+          al =>
+            // skip `ast`, because it uses a 3-letter code which conflicts with `as`
+            // it begins with `a` anyway so still is top of the list
+            al.code2 !== 'ast' && al.code2.startsWith(a.value),
+        )
+        const bIsCommon = !!APP_LANGUAGES.find(
+          al =>
+            // ditto
+            al.code2 !== 'ast' && al.code2.startsWith(b.value),
+        )
+        if (aIsCommon && !bIsCommon) return -1
+        if (bIsCommon && !aIsCommon) return 1
+        // fall back to alphabetical
+        return a.label.localeCompare(b.label)
+      })
+  }, [appLanguage, contentLanguages])
+
+  const currentLanguageLabel =
+    languages.find(lang => lang.value === value)?.label ?? _(msg`All languages`)
+
+  return (
+    <Menu.Root>
+      <Menu.Trigger
+        label={_(
+          msg`Filter search by language (currently: ${currentLanguageLabel})`,
+        )}>
+        {({props}) => (
+          <Button
+            {...props}
+            label={props.accessibilityLabel}
+            size="small"
+            color={platform({native: 'primary', default: 'secondary'})}
+            variant={platform({native: 'ghost', default: 'solid'})}
+            style={native([
+              a.py_sm,
+              a.px_sm,
+              {marginRight: tokens.space.sm * -1},
+            ])}>
+            <ButtonIcon icon={EarthIcon} />
+            <ButtonText>{currentLanguageLabel}</ButtonText>
+            <ButtonIcon
+              icon={platform({
+                native: ChevronUpDownIcon,
+                default: ChevronDownIcon,
+              })}
+            />
+          </Button>
+        )}
+      </Menu.Trigger>
+      <Menu.Outer>
+        <Menu.LabelText>
+          <Trans>Filter search by language</Trans>
+        </Menu.LabelText>
+        <Menu.Item label={_(msg`All languages`)} onPress={() => onChange('')}>
+          <Menu.ItemText>
+            <Trans>All languages</Trans>
+          </Menu.ItemText>
+          <Menu.ItemRadio selected={value === ''} />
+        </Menu.Item>
+        <Menu.Divider />
+        <Menu.Group>
+          {languages.map(lang => (
+            <Menu.Item
+              key={lang.key}
+              label={lang.label}
+              onPress={() => onChange(lang.value)}>
+              <Menu.ItemText>{lang.label}</Menu.ItemText>
+              <Menu.ItemRadio selected={value === lang.value} />
+            </Menu.Item>
+          ))}
+        </Menu.Group>
+      </Menu.Outer>
+    </Menu.Root>
+  )
+}
diff --git a/src/screens/Search/components/StarterPackCard.tsx b/src/screens/Search/components/StarterPackCard.tsx
new file mode 100644
index 000000000..9520dd5a7
--- /dev/null
+++ b/src/screens/Search/components/StarterPackCard.tsx
@@ -0,0 +1,296 @@
+import React from 'react'
+import {View} from 'react-native'
+import {
+  type AppBskyGraphDefs,
+  AppBskyGraphStarterpack,
+  moderateProfile,
+} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {useSession} from '#/state/session'
+import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
+import {ButtonText} from '#/components/Button'
+import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {Link} from '#/components/Link'
+import {MediaInsetBorder} from '#/components/MediaInsetBorder'
+import {useStarterPackLink} from '#/components/StarterPack/StarterPackCard'
+import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
+
+export function StarterPackCard({
+  view,
+}: {
+  view: AppBskyGraphDefs.StarterPackView
+}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const {currentAccount} = useSession()
+  const {gtPhone} = useBreakpoints()
+  const link = useStarterPackLink({view})
+
+  if (
+    !bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
+      view.record,
+      AppBskyGraphStarterpack.isRecord,
+    )
+  ) {
+    return null
+  }
+
+  const profileCount = gtPhone ? 11 : 8
+  const profiles = view.listItemsSample
+    ?.slice(0, profileCount)
+    .map(item => item.subject)
+
+  return (
+    <View
+      style={[
+        a.w_full,
+        a.p_lg,
+        a.gap_md,
+        a.border,
+        a.rounded_sm,
+        a.overflow_hidden,
+        t.atoms.border_contrast_low,
+      ]}>
+      <View aria-hidden style={[a.absolute, a.inset_0, a.z_40]}>
+        <Link
+          to={link.to}
+          label={link.label}
+          style={[a.absolute, a.inset_0]}
+          onHoverIn={link.precache}
+          onPress={link.precache}>
+          <View />
+        </Link>
+      </View>
+
+      <AvatarStack
+        profiles={profiles ?? []}
+        numPending={profileCount}
+        total={view.list?.listItemCount}
+      />
+
+      <View
+        style={[
+          a.w_full,
+          a.flex_row,
+          a.align_start,
+          a.gap_lg,
+          web({
+            position: 'static',
+            zIndex: 'unset',
+          }),
+        ]}>
+        <View style={[a.flex_1]}>
+          <Text
+            emoji
+            style={[a.text_md, a.font_bold, a.leading_snug]}
+            numberOfLines={1}>
+            {view.record.name}
+          </Text>
+          <Text
+            emoji
+            style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}
+            numberOfLines={1}>
+            {view.creator?.did === currentAccount?.did
+              ? _(msg`By you`)
+              : _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)}
+          </Text>
+        </View>
+        <Link
+          to={link.to}
+          label={link.label}
+          onHoverIn={link.precache}
+          onPress={link.precache}
+          variant="solid"
+          color="secondary"
+          size="small"
+          style={[a.z_50]}>
+          <ButtonText>
+            <Trans>Open pack</Trans>
+          </ButtonText>
+        </Link>
+      </View>
+    </View>
+  )
+}
+
+export function AvatarStack({
+  profiles,
+  numPending,
+  total,
+}: {
+  profiles: bsky.profile.AnyProfileView[]
+  numPending: number
+  total?: number
+}) {
+  const t = useTheme()
+  const {gtPhone} = useBreakpoints()
+  const moderationOpts = useModerationOpts()
+  const computedTotal = (total ?? numPending) - numPending
+  const circlesCount = numPending + 1 // add total at end
+  const widthPerc = 100 / circlesCount
+  const [size, setSize] = React.useState<number | null>(null)
+
+  const isPending = (numPending && profiles.length === 0) || !moderationOpts
+
+  const items = isPending
+    ? Array.from({length: numPending ?? circlesCount}).map((_, i) => ({
+        key: i,
+        profile: null,
+        moderation: null,
+      }))
+    : profiles.map(item => ({
+        key: item.did,
+        profile: item,
+        moderation: moderateProfile(item, moderationOpts),
+      }))
+
+  return (
+    <View
+      style={[
+        a.w_full,
+        a.flex_row,
+        a.align_center,
+        a.relative,
+        {width: `${100 - widthPerc * 0.2}%`},
+      ]}>
+      {items.map((item, i) => (
+        <View
+          key={item.key}
+          style={[
+            {
+              width: `${widthPerc}%`,
+              zIndex: 100 - i,
+            },
+          ]}>
+          <View
+            style={[
+              a.relative,
+              {
+                width: '120%',
+              },
+            ]}>
+            <View
+              onLayout={e => setSize(e.nativeEvent.layout.width)}
+              style={[
+                a.rounded_full,
+                t.atoms.bg_contrast_25,
+                {
+                  paddingTop: '100%',
+                },
+              ]}>
+              {size && item.profile ? (
+                <UserAvatar
+                  size={size}
+                  avatar={item.profile.avatar}
+                  type={item.profile.associated?.labeler ? 'labeler' : 'user'}
+                  moderation={item.moderation.ui('avatar')}
+                  style={[a.absolute, a.inset_0]}
+                />
+              ) : (
+                <MediaInsetBorder style={[a.rounded_full]} />
+              )}
+            </View>
+          </View>
+        </View>
+      ))}
+      <View
+        style={[
+          {
+            width: `${widthPerc}%`,
+            zIndex: 1,
+          },
+        ]}>
+        <View
+          style={[
+            a.relative,
+            {
+              width: '120%',
+            },
+          ]}>
+          <View
+            style={[
+              {
+                paddingTop: '100%',
+              },
+            ]}>
+            <View
+              style={[
+                a.absolute,
+                a.inset_0,
+                a.rounded_full,
+                a.align_center,
+                a.justify_center,
+                {
+                  backgroundColor: t.atoms.text_contrast_low.color,
+                },
+              ]}>
+              {computedTotal > 0 ? (
+                <Text
+                  style={[
+                    gtPhone ? a.text_md : a.text_sm,
+                    a.font_bold,
+                    a.leading_snug,
+                    {color: 'white'},
+                  ]}>
+                  <Trans comment="Indicates the number of additional profiles are in the Starter Pack e.g. +12">
+                    +{computedTotal}
+                  </Trans>
+                </Text>
+              ) : (
+                <Plus fill="white" />
+              )}
+            </View>
+          </View>
+        </View>
+      </View>
+    </View>
+  )
+}
+
+export function StarterPackCardSkeleton() {
+  const t = useTheme()
+  const {gtPhone} = useBreakpoints()
+
+  const profileCount = gtPhone ? 11 : 8
+
+  return (
+    <View
+      style={[
+        a.w_full,
+        a.p_lg,
+        a.gap_md,
+        a.border,
+        a.rounded_sm,
+        a.overflow_hidden,
+        t.atoms.border_contrast_low,
+      ]}>
+      <AvatarStack profiles={[]} numPending={profileCount} />
+
+      <View
+        style={[
+          a.w_full,
+          a.flex_row,
+          a.align_start,
+          a.gap_lg,
+          web({
+            position: 'static',
+            zIndex: 'unset',
+          }),
+        ]}>
+        <View style={[a.flex_1, a.gap_xs]}>
+          <LoadingPlaceholder width={180} height={18} />
+          <LoadingPlaceholder width={120} height={14} />
+        </View>
+
+        <LoadingPlaceholder width={100} height={33} />
+      </View>
+    </View>
+  )
+}
diff --git a/src/screens/Search/index.tsx b/src/screens/Search/index.tsx
new file mode 100644
index 000000000..429f1e5c7
--- /dev/null
+++ b/src/screens/Search/index.tsx
@@ -0,0 +1,13 @@
+import {
+  type NativeStackScreenProps,
+  type SearchTabNavigatorParams,
+} from '#/lib/routes/types'
+import {SearchScreenShell} from './Shell'
+
+export function SearchScreen(
+  props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
+) {
+  const queryParam = props.route?.params?.q ?? ''
+
+  return <SearchScreenShell queryParam={queryParam} testID="searchScreen" />
+}
diff --git a/src/screens/Search/modules/ExploreFeedPreviews.tsx b/src/screens/Search/modules/ExploreFeedPreviews.tsx
new file mode 100644
index 000000000..30aa00a3f
--- /dev/null
+++ b/src/screens/Search/modules/ExploreFeedPreviews.tsx
@@ -0,0 +1,264 @@
+import {useMemo} from 'react'
+import {type AppBskyFeedDefs, moderatePost} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useInfiniteQuery} from '@tanstack/react-query'
+
+import {CustomFeedAPI} from '#/lib/api/feed/custom'
+import {aggregateUserInterests} from '#/lib/api/feed/utils'
+import {FeedTuner} from '#/lib/api/feed-manip'
+import {cleanError} from '#/lib/strings/errors'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {
+  type FeedPostSlice,
+  type FeedPostSliceItem,
+} from '#/state/queries/post-feed'
+import {usePreferencesQuery} from '#/state/queries/preferences'
+import {useAgent} from '#/state/session'
+
+const RQKEY_ROOT = 'feed-previews'
+const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds]
+
+const LIMIT = 8 // sliced to 6, overfetch to account for moderation
+
+export type FeedPreviewItem =
+  | {
+      type: 'topBorder'
+      key: string
+    }
+  | {
+      type: 'preview:loading'
+      key: string
+    }
+  | {
+      type: 'preview:error'
+      key: string
+      message: string
+      error: string
+    }
+  | {
+      type: 'preview:loadMoreError'
+      key: string
+    }
+  | {
+      type: 'preview:empty'
+      key: string
+    }
+  | {
+      type: 'preview:header'
+      key: string
+      feed: AppBskyFeedDefs.GeneratorView
+    }
+  | {
+      type: 'preview:footer'
+      key: string
+    }
+  // copied from PostFeed.tsx
+  | {
+      type: 'preview:sliceItem'
+      key: string
+      slice: FeedPostSlice
+      indexInSlice: number
+      showReplyTo: boolean
+      hideTopBorder: boolean
+    }
+  | {
+      type: 'preview:sliceViewFullThread'
+      key: string
+      uri: string
+    }
+
+export function useFeedPreviews(feeds: AppBskyFeedDefs.GeneratorView[]) {
+  const uris = feeds.map(feed => feed.uri)
+  const {_} = useLingui()
+  const agent = useAgent()
+  const {data: preferences} = usePreferencesQuery()
+  const userInterests = aggregateUserInterests(preferences)
+  const moderationOpts = useModerationOpts()
+  const enabled = feeds.length > 0
+
+  const query = useInfiniteQuery({
+    enabled,
+    queryKey: RQKEY(uris),
+    queryFn: async ({pageParam}) => {
+      const feed = feeds[pageParam]
+      const api = new CustomFeedAPI({
+        agent,
+        feedParams: {feed: feed.uri},
+        userInterests,
+      })
+      const data = await api.fetch({cursor: undefined, limit: LIMIT})
+      return {
+        feed,
+        posts: data.feed,
+      }
+    },
+    initialPageParam: 0,
+    getNextPageParam: (_p, _a, count) =>
+      count < feeds.length ? count + 1 : undefined,
+  })
+
+  const {data, isFetched, isError, isPending, error} = query
+
+  return {
+    query,
+    data: useMemo<FeedPreviewItem[]>(() => {
+      const items: FeedPreviewItem[] = []
+
+      if (!enabled) return items
+
+      const isEmpty =
+        !isPending && !data?.pages?.some(page => page.posts.length)
+
+      if (isFetched) {
+        if (isError && isEmpty) {
+          items.push({
+            type: 'preview:error',
+            key: 'error',
+            message: _(msg`An error occurred while fetching the feed.`),
+            error: cleanError(error),
+          })
+        } else if (isEmpty) {
+          items.push({
+            type: 'preview:empty',
+            key: 'empty',
+          })
+        } else if (data) {
+          for (let pageIndex = 0; pageIndex < data.pages.length; pageIndex++) {
+            const page = data.pages[pageIndex]
+            // default feed tuner - we just want it to slice up the feed
+            const tuner = new FeedTuner([])
+            const slices: FeedPreviewItem[] = []
+
+            let rowIndex = 0
+            for (const item of tuner.tune(page.posts)) {
+              if (item.isFallbackMarker) continue
+
+              const moderations = item.items.map(item =>
+                moderatePost(item.post, moderationOpts!),
+              )
+
+              // apply moderation filters
+              item.items = item.items.filter((_, i) => {
+                return !moderations[i]?.ui('contentList').filter
+              })
+
+              const slice = {
+                _reactKey: item._reactKey,
+                _isFeedPostSlice: true,
+                isFallbackMarker: false,
+                isIncompleteThread: item.isIncompleteThread,
+                feedContext: item.feedContext,
+                reason: item.reason,
+                feedPostUri: item.feedPostUri,
+                items: item.items.slice(0, 6).map((subItem, i) => {
+                  const feedPostSliceItem: FeedPostSliceItem = {
+                    _reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
+                    uri: subItem.post.uri,
+                    post: subItem.post,
+                    record: subItem.record,
+                    moderation: moderations[i],
+                    parentAuthor: subItem.parentAuthor,
+                    isParentBlocked: subItem.isParentBlocked,
+                    isParentNotFound: subItem.isParentNotFound,
+                  }
+                  return feedPostSliceItem
+                }),
+              }
+              if (slice.isIncompleteThread && slice.items.length >= 3) {
+                const beforeLast = slice.items.length - 2
+                const last = slice.items.length - 1
+                slices.push({
+                  type: 'preview:sliceItem',
+                  key: slice.items[0]._reactKey,
+                  slice: slice,
+                  indexInSlice: 0,
+                  showReplyTo: false,
+                  hideTopBorder: rowIndex === 0,
+                })
+                slices.push({
+                  type: 'preview:sliceViewFullThread',
+                  key: slice._reactKey + '-viewFullThread',
+                  uri: slice.items[0].uri,
+                })
+                slices.push({
+                  type: 'preview:sliceItem',
+                  key: slice.items[beforeLast]._reactKey,
+                  slice: slice,
+                  indexInSlice: beforeLast,
+                  showReplyTo:
+                    slice.items[beforeLast].parentAuthor?.did !==
+                    slice.items[beforeLast].post.author.did,
+                  hideTopBorder: false,
+                })
+                slices.push({
+                  type: 'preview:sliceItem',
+                  key: slice.items[last]._reactKey,
+                  slice: slice,
+                  indexInSlice: last,
+                  showReplyTo: false,
+                  hideTopBorder: false,
+                })
+              } else {
+                for (let i = 0; i < slice.items.length; i++) {
+                  slices.push({
+                    type: 'preview:sliceItem',
+                    key: slice.items[i]._reactKey,
+                    slice: slice,
+                    indexInSlice: i,
+                    showReplyTo: i === 0,
+                    hideTopBorder: i === 0 && rowIndex === 0,
+                  })
+                }
+              }
+
+              rowIndex++
+            }
+
+            if (slices.length > 0) {
+              if (pageIndex > 0) {
+                items.push({
+                  type: 'topBorder',
+                  key: `topBorder-${page.feed.uri}`,
+                })
+              }
+              items.push(
+                {
+                  type: 'preview:footer',
+                  key: `footer-${page.feed.uri}`,
+                },
+                {
+                  type: 'preview:header',
+                  key: `header-${page.feed.uri}`,
+                  feed: page.feed,
+                },
+                ...slices,
+              )
+            }
+          }
+        } else if (isError && !isEmpty) {
+          items.push({
+            type: 'preview:loadMoreError',
+            key: 'loadMoreError',
+          })
+        }
+      } else {
+        items.push({
+          type: 'preview:loading',
+          key: 'loading',
+        })
+      }
+
+      return items
+    }, [
+      enabled,
+      data,
+      isFetched,
+      isError,
+      isPending,
+      moderationOpts,
+      _,
+      error,
+    ]),
+  }
+}
diff --git a/src/screens/Search/components/ExploreRecommendations.tsx b/src/screens/Search/modules/ExploreRecommendations.tsx
index 602bab87d..4cf84269a 100644
--- a/src/screens/Search/components/ExploreRecommendations.tsx
+++ b/src/screens/Search/modules/ExploreRecommendations.tsx
@@ -1,8 +1,8 @@
 import {View} from 'react-native'
-import {AppBskyUnspeccedDefs} from '@atproto/api'
+import {type AppBskyUnspeccedDefs} from '@atproto/api'
 import {Trans} from '@lingui/macro'
 
-import {logEvent} from '#/lib/statsig/statsig'
+import {logger} from '#/logger'
 import {isWeb} from '#/platform/detection'
 import {
   DEFAULT_LIMIT as RECOMMENDATIONS_COUNT,
@@ -18,6 +18,8 @@ import {
 } from '#/components/TrendingTopics'
 import {Text} from '#/components/Typography'
 
+// Note: This module is not currently used and may be removed in the future.
+
 export function ExploreRecommendations() {
   const {enabled} = useTrendingConfig()
   return enabled ? <Inner /> : null
@@ -86,7 +88,11 @@ function Inner() {
                   key={topic.link}
                   topic={topic}
                   onPress={() => {
-                    logEvent('recommendedTopic:click', {context: 'explore'})
+                    logger.metric(
+                      'recommendedTopic:click',
+                      {context: 'explore'},
+                      {statsig: true},
+                    )
                   }}>
                   {({hovered}) => (
                     <TrendingTopic
diff --git a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
new file mode 100644
index 000000000..070d75910
--- /dev/null
+++ b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx
@@ -0,0 +1,228 @@
+import {memo, useEffect} from 'react'
+import {View} from 'react-native'
+import {type AppBskyActorSearchActors, type ModerationOpts} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {type InfiniteData} from '@tanstack/react-query'
+
+import {logger} from '#/logger'
+import {usePreferencesQuery} from '#/state/queries/preferences'
+import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
+import {
+  popularInterests,
+  useInterestsDisplayNames,
+} from '#/screens/Onboarding/state'
+import {useTheme} from '#/alf'
+import {atoms as a} from '#/alf'
+import {Button} from '#/components/Button'
+import * as ProfileCard from '#/components/ProfileCard'
+import {boostInterests, Tabs} from '#/components/ProgressGuide/FollowDialog'
+import {Text} from '#/components/Typography'
+import type * as bsky from '#/types/bsky'
+
+export function useLoadEnoughProfiles({
+  interest,
+  data,
+  isLoading,
+  isFetchingNextPage,
+  hasNextPage,
+  fetchNextPage,
+}: {
+  interest: string | null
+  data?: InfiniteData<AppBskyActorSearchActors.OutputSchema>
+  isLoading: boolean
+  isFetchingNextPage: boolean
+  hasNextPage: boolean
+  fetchNextPage: () => Promise<unknown>
+}) {
+  const profileCount =
+    data?.pages.flatMap(page =>
+      page.actors.filter(actor => !actor.viewer?.following),
+    ).length || 0
+  const isAnyLoading = isLoading || isFetchingNextPage
+  const isEnoughProfiles = profileCount > 3
+  const shouldFetchMore = !isEnoughProfiles && hasNextPage && !!interest
+  useEffect(() => {
+    if (shouldFetchMore && !isAnyLoading) {
+      logger.info('Not enough suggested accounts - fetching more')
+      fetchNextPage()
+    }
+  }, [shouldFetchMore, fetchNextPage, isAnyLoading, interest])
+
+  return {
+    isReady: !shouldFetchMore,
+  }
+}
+
+export function SuggestedAccountsTabBar({
+  selectedInterest,
+  onSelectInterest,
+}: {
+  selectedInterest: string | null
+  onSelectInterest: (interest: string | null) => void
+}) {
+  const {_} = useLingui()
+  const interestsDisplayNames = useInterestsDisplayNames()
+  const {data: preferences} = usePreferencesQuery()
+  const personalizedInterests = preferences?.interests?.tags
+  const interests = Object.keys(interestsDisplayNames)
+    .sort(boostInterests(popularInterests))
+    .sort(boostInterests(personalizedInterests))
+  return (
+    <BlockDrawerGesture>
+      <Tabs
+        interests={['all', ...interests]}
+        selectedInterest={selectedInterest || 'all'}
+        onSelectTab={tab => {
+          logger.metric(
+            'explore:suggestedAccounts:tabPressed',
+            {tab: tab},
+            {statsig: true},
+          )
+          onSelectInterest(tab === 'all' ? null : tab)
+        }}
+        hasSearchText={false}
+        interestsDisplayNames={{
+          all: _(msg`All`),
+          ...interestsDisplayNames,
+        }}
+        TabComponent={Tab}
+      />
+    </BlockDrawerGesture>
+  )
+}
+
+let Tab = ({
+  onSelectTab,
+  interest,
+  active,
+  index,
+  interestsDisplayName,
+  onLayout,
+}: {
+  onSelectTab: (index: number) => void
+  interest: string
+  active: boolean
+  index: number
+  interestsDisplayName: string
+  onLayout: (index: number, x: number, width: number) => void
+}): React.ReactNode => {
+  const t = useTheme()
+  const {_} = useLingui()
+  const activeText = active ? _(msg` (active)`) : ''
+  return (
+    <View
+      key={interest}
+      onLayout={e =>
+        onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width)
+      }>
+      <Button
+        label={_(msg`Search for "${interestsDisplayName}"${activeText}`)}
+        onPress={() => onSelectTab(index)}>
+        {({hovered, pressed, focused}) => (
+          <View
+            style={[
+              a.rounded_full,
+              a.px_lg,
+              a.py_sm,
+              a.border,
+              active || hovered || pressed || focused
+                ? [
+                    t.atoms.bg_contrast_25,
+                    {borderColor: t.atoms.bg_contrast_25.backgroundColor},
+                  ]
+                : [t.atoms.bg, t.atoms.border_contrast_low],
+            ]}>
+            <Text
+              style={[
+                /* TODO: medium weight */
+                active || hovered || pressed || focused
+                  ? t.atoms.text
+                  : t.atoms.text_contrast_medium,
+              ]}>
+              {interestsDisplayName}
+            </Text>
+          </View>
+        )}
+      </Button>
+    </View>
+  )
+}
+Tab = memo(Tab)
+
+/**
+ * Profile card for suggested accounts. Note: border is on the bottom edge
+ */
+let SuggestedProfileCard = ({
+  profile,
+  moderationOpts,
+  recId,
+  position,
+}: {
+  profile: bsky.profile.AnyProfileView
+  moderationOpts: ModerationOpts
+  recId?: number
+  position: number
+}): React.ReactNode => {
+  const t = useTheme()
+  return (
+    <ProfileCard.Link
+      profile={profile}
+      style={[a.flex_1]}
+      onPress={() => {
+        logger.metric(
+          'suggestedUser:press',
+          {
+            logContext: 'Explore',
+            recId,
+            position,
+          },
+          {statsig: true},
+        )
+      }}>
+      <View
+        style={[
+          a.w_full,
+          a.py_lg,
+          a.px_lg,
+          a.border_t,
+          t.atoms.border_contrast_low,
+          a.flex_1,
+        ]}>
+        <ProfileCard.Outer>
+          <ProfileCard.Header>
+            <ProfileCard.Avatar
+              profile={profile}
+              moderationOpts={moderationOpts}
+            />
+            <ProfileCard.NameAndHandle
+              profile={profile}
+              moderationOpts={moderationOpts}
+            />
+            <ProfileCard.FollowButton
+              profile={profile}
+              moderationOpts={moderationOpts}
+              withIcon={false}
+              logContext="ExploreSuggestedAccounts"
+              onFollow={() => {
+                logger.metric(
+                  'suggestedUser:follow',
+                  {
+                    logContext: 'Explore',
+                    location: 'Card',
+                    recId,
+                    position,
+                  },
+                  {statsig: true},
+                )
+              }}
+            />
+          </ProfileCard.Header>
+          <ProfileCard.Description profile={profile} numberOfLines={2} />
+        </ProfileCard.Outer>
+      </View>
+    </ProfileCard.Link>
+  )
+}
+SuggestedProfileCard = memo(SuggestedProfileCard)
+export {SuggestedProfileCard}
diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx
new file mode 100644
index 000000000..88d16b393
--- /dev/null
+++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx
@@ -0,0 +1,278 @@
+import {Pressable, View} from 'react-native'
+import {type AppBskyUnspeccedDefs} from '@atproto/api'
+import {msg, plural, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {logger} from '#/logger'
+import {useTrendingSettings} from '#/state/preferences/trending'
+import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery'
+import {useTrendingConfig} from '#/state/trending-config'
+import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
+import {formatCount} from '#/view/com/util/numeric/format'
+import {atoms as a, useGutters, useTheme, type ViewStyleProp, web} from '#/alf'
+import {AvatarStack} from '#/components/AvatarStack'
+import {type Props as SVGIconProps} from '#/components/icons/common'
+import {Flame_Stroke2_Corner1_Rounded as FlameIcon} from '#/components/icons/Flame'
+import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
+import {Link} from '#/components/Link'
+import {Text} from '#/components/Typography'
+
+const TOPIC_COUNT = 5
+
+export function ExploreTrendingTopics() {
+  const {enabled} = useTrendingConfig()
+  const {trendingDisabled} = useTrendingSettings()
+  return enabled && !trendingDisabled ? <Inner /> : null
+}
+
+function Inner() {
+  const {data: trending, error, isLoading} = useGetTrendsQuery()
+  const noTopics = !isLoading && !error && !trending?.trends?.length
+
+  return isLoading ? (
+    Array.from({length: TOPIC_COUNT}).map((__, i) => (
+      <TrendingTopicRowSkeleton key={i} withPosts={i === 0} />
+    ))
+  ) : error || !trending?.trends || noTopics ? null : (
+    <>
+      {trending.trends.map((trend, index) => (
+        <TrendRow
+          key={trend.link}
+          trend={trend}
+          rank={index + 1}
+          onPress={() => {
+            logger.metric('trendingTopic:click', {context: 'explore'})
+          }}
+        />
+      ))}
+    </>
+  )
+}
+
+export function TrendRow({
+  trend,
+  rank,
+  children,
+  onPress,
+}: ViewStyleProp & {
+  trend: AppBskyUnspeccedDefs.TrendView
+  rank: number
+  children?: React.ReactNode
+  onPress?: () => void
+}) {
+  const t = useTheme()
+  const {_, i18n} = useLingui()
+  const gutters = useGutters([0, 'base'])
+
+  const category = useCategoryDisplayName(trend?.category || 'other')
+  const age = Math.floor(
+    (Date.now() - new Date(trend.startedAt || Date.now()).getTime()) /
+      (1000 * 60 * 60),
+  )
+  const badgeType = trend.status === 'hot' ? 'hot' : age < 2 ? 'new' : age
+  const postCount = trend.postCount
+    ? _(
+        plural(trend.postCount, {
+          other: `${formatCount(i18n, trend.postCount)} posts`,
+        }),
+      )
+    : null
+
+  return (
+    <Link
+      testID={trend.link}
+      label={_(msg`Browse topic ${trend.displayName}`)}
+      to={trend.link}
+      onPress={onPress}
+      style={[a.border_b, t.atoms.border_contrast_low]}
+      PressableComponent={Pressable}>
+      {({hovered, pressed}) => (
+        <>
+          <View
+            style={[
+              gutters,
+              a.w_full,
+              a.py_lg,
+              a.flex_row,
+              a.gap_2xs,
+              (hovered || pressed) && t.atoms.bg_contrast_25,
+            ]}>
+            <View style={[a.flex_1, a.gap_xs]}>
+              <View style={[a.flex_row]}>
+                <Text
+                  style={[a.text_md, a.font_bold, a.leading_snug, {width: 20}]}>
+                  <Trans comment='The trending topic rank, i.e. "1. March Madness", "2. The Bachelor"'>
+                    {rank}.
+                  </Trans>
+                </Text>
+                <Text
+                  style={[a.text_md, a.font_bold, a.leading_snug]}
+                  numberOfLines={1}>
+                  {trend.displayName}
+                </Text>
+              </View>
+              <View
+                style={[
+                  a.flex_row,
+                  a.gap_sm,
+                  a.align_center,
+                  {paddingLeft: 20},
+                ]}>
+                {trend.actors.length > 0 && (
+                  <AvatarStack size={20} profiles={trend.actors} />
+                )}
+                <Text
+                  style={[
+                    a.text_sm,
+                    t.atoms.text_contrast_medium,
+                    web(a.leading_snug),
+                  ]}
+                  numberOfLines={1}>
+                  {postCount}
+                  {postCount && category && <> &middot; </>}
+                  {category}
+                </Text>
+              </View>
+            </View>
+            <View style={[a.flex_shrink_0]}>
+              <TrendingIndicator type={badgeType} />
+            </View>
+          </View>
+
+          {children}
+        </>
+      )}
+    </Link>
+  )
+}
+
+type TrendingIndicatorType = 'hot' | 'new' | number
+
+function TrendingIndicator({type}: {type: TrendingIndicatorType | 'skeleton'}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const pillStyles = [
+    a.flex_row,
+    a.align_center,
+    a.gap_xs,
+    a.rounded_full,
+    a.px_sm,
+    {
+      height: 28,
+    },
+  ]
+
+  let Icon: React.ComponentType<SVGIconProps> | null = null
+  let text: string | null = null
+  let color: string | null = null
+  let backgroundColor: string | null = null
+
+  switch (type) {
+    case 'skeleton': {
+      return (
+        <View
+          style={[
+            pillStyles,
+            {backgroundColor: t.palette.contrast_25, width: 65, height: 28},
+          ]}
+        />
+      )
+    }
+    case 'hot': {
+      Icon = FlameIcon
+      color =
+        t.scheme === 'light' ? t.palette.negative_500 : t.palette.negative_950
+      backgroundColor =
+        t.scheme === 'light' ? t.palette.negative_50 : t.palette.negative_200
+      text = _(msg`Hot`)
+      break
+    }
+    case 'new': {
+      Icon = TrendingIcon
+      text = _(msg`New`)
+      color = t.palette.positive_700
+      backgroundColor = t.palette.positive_50
+      break
+    }
+    default: {
+      text = _(
+        msg({
+          message: `${type}h ago`,
+          comment:
+            'trending topic time spent trending. should be as short as possible to fit in a pill',
+        }),
+      )
+      color = t.atoms.text_contrast_medium.color
+      backgroundColor = t.atoms.bg_contrast_25.backgroundColor
+      break
+    }
+  }
+
+  return (
+    <View style={[pillStyles, {backgroundColor}]}>
+      {Icon && <Icon size="sm" style={{color}} />}
+      <Text style={[a.text_sm, {color}]}>{text}</Text>
+    </View>
+  )
+}
+
+function useCategoryDisplayName(
+  category: AppBskyUnspeccedDefs.TrendView['category'],
+) {
+  const {_} = useLingui()
+
+  switch (category) {
+    case 'sports':
+      return _(msg`Sports`)
+    case 'politics':
+      return _(msg`Politics`)
+    case 'video-games':
+      return _(msg`Video Games`)
+    case 'pop-culture':
+      return _(msg`Entertainment`)
+    case 'news':
+      return _(msg`News`)
+    case 'other':
+    default:
+      return null
+  }
+}
+
+export function TrendingTopicRowSkeleton({}: {withPosts: boolean}) {
+  const t = useTheme()
+  const gutters = useGutters([0, 'base'])
+
+  return (
+    <View
+      style={[
+        gutters,
+        a.w_full,
+        a.py_lg,
+        a.flex_row,
+        a.gap_2xs,
+        a.border_b,
+        t.atoms.border_contrast_low,
+      ]}>
+      <View style={[a.flex_1, a.gap_sm]}>
+        <View style={[a.flex_row, a.align_center]}>
+          <View style={[{width: 20}]}>
+            <LoadingPlaceholder
+              width={12}
+              height={12}
+              style={[a.rounded_full]}
+            />
+          </View>
+          <LoadingPlaceholder width={90} height={18} />
+        </View>
+        <View style={[a.flex_row, a.gap_sm, a.align_center, {paddingLeft: 20}]}>
+          <LoadingPlaceholder width={70} height={18} />
+          <LoadingPlaceholder width={40} height={18} />
+          <LoadingPlaceholder width={60} height={18} />
+        </View>
+      </View>
+      <View style={[a.flex_shrink_0]}>
+        <TrendingIndicator type="skeleton" />
+      </View>
+    </View>
+  )
+}
diff --git a/src/screens/Search/components/ExploreTrendingVideos.tsx b/src/screens/Search/modules/ExploreTrendingVideos.tsx
index 00fa76dbf..54eb73312 100644
--- a/src/screens/Search/components/ExploreTrendingVideos.tsx
+++ b/src/screens/Search/modules/ExploreTrendingVideos.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import {useMemo} from 'react'
 import {ScrollView, View} from 'react-native'
 import {AppBskyEmbedVideo, AtUri} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
@@ -8,18 +8,12 @@ import {useQueryClient} from '@tanstack/react-query'
 
 import {VIDEO_FEED_URI} from '#/lib/constants'
 import {makeCustomFeedLink} from '#/lib/routes/links'
-import {logEvent} from '#/lib/statsig/statsig'
-import {isWeb} from '#/platform/detection'
-import {useSavedFeeds} from '#/state/queries/feed'
+import {logger} from '#/logger'
 import {RQKEY, usePostFeedQuery} from '#/state/queries/post-feed'
-import {useAddSavedFeedsMutation} from '#/state/queries/preferences'
 import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
 import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
-import {Button, ButtonIcon, ButtonText} from '#/components/Button'
-import {GradientFill} from '#/components/GradientFill'
+import {ButtonIcon} from '#/components/Button'
 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
-import {Pin_Stroke2_Corner0_Rounded as Pin} from '#/components/icons/Pin'
-import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
 import {Link} from '#/components/Link'
 import {Text} from '#/components/Typography'
 import {
@@ -37,7 +31,6 @@ const FEED_PARAMS: {
 }
 
 export function ExploreTrendingVideos() {
-  const t = useTheme()
   const {_} = useLingui()
   const gutters = useGutters([0, 'base'])
   const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS)
@@ -55,30 +48,30 @@ export function ExploreTrendingVideos() {
     }
   })
 
-  const {data: saved} = useSavedFeeds()
-  const isSavedAlready = React.useMemo(() => {
-    return !!saved?.feeds?.some(info => info.config.value === VIDEO_FEED_URI)
-  }, [saved])
-
-  const {mutateAsync: addSavedFeeds, isPending: isPinPending} =
-    useAddSavedFeedsMutation()
-  const pinFeed = React.useCallback(
-    (e: any) => {
-      e.preventDefault()
-
-      addSavedFeeds([
-        {
-          type: 'feed',
-          value: VIDEO_FEED_URI,
-          pinned: true,
-        },
-      ])
-
-      // prevent navigation
-      return false
-    },
-    [addSavedFeeds],
-  )
+  // const {data: saved} = useSavedFeeds()
+  // const isSavedAlready = useMemo(() => {
+  //   return !!saved?.feeds?.some(info => info.config.value === VIDEO_FEED_URI)
+  // }, [saved])
+
+  // const {mutateAsync: addSavedFeeds, isPending: isPinPending} =
+  //   useAddSavedFeedsMutation()
+  // const pinFeed = useCallback(
+  //   (e: any) => {
+  //     e.preventDefault()
+
+  //     addSavedFeeds([
+  //       {
+  //         type: 'feed',
+  //         value: VIDEO_FEED_URI,
+  //         pinned: true,
+  //       },
+  //     ])
+
+  //     // prevent navigation
+  //     return false
+  //   },
+  //   [addSavedFeeds],
+  // )
 
   if (error) {
     return null
@@ -86,38 +79,6 @@ export function ExploreTrendingVideos() {
 
   return (
     <View style={[a.pb_xl]}>
-      <View
-        style={[
-          a.flex_row,
-          isWeb
-            ? [a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
-            : [a.p_lg, a.pt_xl, a.gap_md],
-          a.border_b,
-          t.atoms.border_contrast_low,
-        ]}>
-        <View style={[a.flex_1, a.gap_sm]}>
-          <View style={[a.flex_row, a.align_center, a.gap_sm]}>
-            <Graph
-              size="lg"
-              fill={t.palette.primary_500}
-              style={{marginLeft: -2}}
-            />
-            <Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>
-              <Trans>Trending Videos</Trans>
-            </Text>
-            <View style={[a.py_xs, a.px_sm, a.rounded_sm, a.overflow_hidden]}>
-              <GradientFill gradient={tokens.gradients.primary} />
-              <Text style={[a.text_sm, a.font_heavy, {color: 'white'}]}>
-                <Trans>BETA</Trans>
-              </Text>
-            </View>
-          </View>
-          <Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
-            <Trans>Popular videos in your network.</Trans>
-          </Text>
-        </View>
-      </View>
-
       <BlockDrawerGesture>
         <ScrollView
           horizontal
@@ -153,7 +114,7 @@ export function ExploreTrendingVideos() {
         </ScrollView>
       </BlockDrawerGesture>
 
-      {!isSavedAlready && (
+      {/* {!isSavedAlready && (
         <View
           style={[
             gutters,
@@ -179,7 +140,7 @@ export function ExploreTrendingVideos() {
             <ButtonIcon icon={Pin} position="right" />
           </Button>
         </View>
-      )}
+      )} */}
     </View>
   )
 }
@@ -191,7 +152,7 @@ function VideoCards({
 }) {
   const t = useTheme()
   const {_} = useLingui()
-  const items = React.useMemo(() => {
+  const items = useMemo(() => {
     return data.pages
       .flatMap(page => page.slices)
       .map(slice => slice.items[0])
@@ -199,7 +160,7 @@ function VideoCards({
       .filter(item => AppBskyEmbedVideo.isView(item.post.embed))
       .slice(0, 8)
   }, [data])
-  const href = React.useMemo(() => {
+  const href = useMemo(() => {
     const urip = new AtUri(VIDEO_FEED_URI)
     return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'explore')
   }, [])
@@ -217,9 +178,11 @@ function VideoCards({
               sourceInterstitial: 'explore',
             }}
             onInteract={() => {
-              logEvent('videoCard:click', {
-                context: 'interstitial:explore',
-              })
+              logger.metric(
+                'videoCard:click',
+                {context: 'interstitial:explore'},
+                {statsig: true},
+              )
             }}
           />
         </View>
diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx
index e28c98803..57b86fb2b 100644
--- a/src/screens/Settings/ContentAndMediaSettings.tsx
+++ b/src/screens/Settings/ContentAndMediaSettings.tsx
@@ -1,8 +1,8 @@
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
-import {CommonNavigatorParams} from '#/lib/routes/types'
+import {type CommonNavigatorParams} from '#/lib/routes/types'
 import {logEvent} from '#/lib/statsig/statsig'
 import {isNative} from '#/platform/detection'
 import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences'
@@ -22,7 +22,7 @@ import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons
 import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home'
 import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
 import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
-import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
+import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
 import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window'
 import * as Layout from '#/components/Layout'
 
diff --git a/src/state/queries/actor-search.ts b/src/state/queries/actor-search.ts
index 6d6c46e04..0b5de2303 100644
--- a/src/state/queries/actor-search.ts
+++ b/src/state/queries/actor-search.ts
@@ -1,9 +1,12 @@
-import {AppBskyActorDefs, AppBskyActorSearchActors} from '@atproto/api'
 import {
-  InfiniteData,
+  type AppBskyActorDefs,
+  type AppBskyActorSearchActors,
+} from '@atproto/api'
+import {
+  type InfiniteData,
   keepPreviousData,
-  QueryClient,
-  QueryKey,
+  type QueryClient,
+  type QueryKey,
   useInfiniteQuery,
   useQuery,
 } from '@tanstack/react-query'
@@ -15,7 +18,11 @@ const RQKEY_ROOT = 'actor-search'
 export const RQKEY = (query: string) => [RQKEY_ROOT, query]
 
 const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
-export const RQKEY_PAGINATED = (query: string) => [RQKEY_ROOT_PAGINATED, query]
+export const RQKEY_PAGINATED = (query: string, limit?: number) => [
+  RQKEY_ROOT_PAGINATED,
+  query,
+  limit,
+]
 
 export function useActorSearch({
   query,
@@ -42,10 +49,12 @@ export function useActorSearchPaginated({
   query,
   enabled,
   maintainData,
+  limit = 25,
 }: {
   query: string
   enabled?: boolean
   maintainData?: boolean
+  limit?: number
 }) {
   const agent = useAgent()
   return useInfiniteQuery<
@@ -56,11 +65,11 @@ export function useActorSearchPaginated({
     string | undefined
   >({
     staleTime: STALE.MINUTES.FIVE,
-    queryKey: RQKEY_PAGINATED(query),
+    queryKey: RQKEY_PAGINATED(query, limit),
     queryFn: async ({pageParam}) => {
       const res = await agent.searchActors({
         q: query,
-        limit: 25,
+        limit,
         cursor: pageParam,
       })
       return res.data
diff --git a/src/state/queries/trending/useGetSuggestedFeedsQuery.ts b/src/state/queries/trending/useGetSuggestedFeedsQuery.ts
new file mode 100644
index 000000000..16522f5c9
--- /dev/null
+++ b/src/state/queries/trending/useGetSuggestedFeedsQuery.ts
@@ -0,0 +1,48 @@
+import {useQuery} from '@tanstack/react-query'
+
+import {
+  aggregateUserInterests,
+  createBskyTopicsHeader,
+} from '#/lib/api/feed/utils'
+import {getContentLanguages} from '#/state/preferences/languages'
+import {STALE} from '#/state/queries'
+import {usePreferencesQuery} from '#/state/queries/preferences'
+import {useAgent} from '#/state/session'
+
+export const DEFAULT_LIMIT = 5
+
+export const createGetTrendsQueryKey = () => ['suggested-feeds']
+
+export function useGetSuggestedFeedsQuery() {
+  const agent = useAgent()
+  const {data: preferences} = usePreferencesQuery()
+  const savedFeeds = preferences?.savedFeeds
+
+  return useQuery({
+    enabled: !!savedFeeds,
+    refetchOnWindowFocus: true,
+    staleTime: STALE.MINUTES.ONE,
+    queryKey: createGetTrendsQueryKey(),
+    queryFn: async () => {
+      const contentLangs = getContentLanguages().join(',')
+      const {data} = await agent.app.bsky.unspecced.getSuggestedFeeds(
+        {
+          limit: DEFAULT_LIMIT,
+        },
+        {
+          headers: {
+            ...createBskyTopicsHeader(aggregateUserInterests(preferences)),
+            'Accept-Language': contentLangs,
+          },
+        },
+      )
+
+      return {
+        feeds: data.feeds.filter(feed => {
+          const isSaved = !!savedFeeds?.find(s => s.value === feed.uri)
+          return !isSaved
+        }),
+      }
+    },
+  })
+}
diff --git a/src/state/queries/trending/useGetTrendsQuery.ts b/src/state/queries/trending/useGetTrendsQuery.ts
new file mode 100644
index 000000000..d96bf0603
--- /dev/null
+++ b/src/state/queries/trending/useGetTrendsQuery.ts
@@ -0,0 +1,59 @@
+import React from 'react'
+import {type AppBskyUnspeccedGetTrends} from '@atproto/api'
+import {hasMutedWord} from '@atproto/api/dist/moderation/mutewords'
+import {useQuery} from '@tanstack/react-query'
+
+import {
+  aggregateUserInterests,
+  createBskyTopicsHeader,
+} from '#/lib/api/feed/utils'
+import {getContentLanguages} from '#/state/preferences/languages'
+import {STALE} from '#/state/queries'
+import {usePreferencesQuery} from '#/state/queries/preferences'
+import {useAgent} from '#/state/session'
+
+export const DEFAULT_LIMIT = 5
+
+export const createGetTrendsQueryKey = () => ['trends']
+
+export function useGetTrendsQuery() {
+  const agent = useAgent()
+  const {data: preferences} = usePreferencesQuery()
+  const mutedWords = React.useMemo(() => {
+    return preferences?.moderationPrefs?.mutedWords || []
+  }, [preferences?.moderationPrefs])
+
+  return useQuery({
+    refetchOnWindowFocus: true,
+    staleTime: STALE.MINUTES.THREE,
+    queryKey: createGetTrendsQueryKey(),
+    queryFn: async () => {
+      const contentLangs = getContentLanguages().join(',')
+      const {data} = await agent.app.bsky.unspecced.getTrends(
+        {
+          limit: DEFAULT_LIMIT,
+        },
+        {
+          headers: {
+            ...createBskyTopicsHeader(aggregateUserInterests(preferences)),
+            'Accept-Language': contentLangs,
+          },
+        },
+      )
+      return data
+    },
+    select: React.useCallback(
+      (data: AppBskyUnspeccedGetTrends.OutputSchema) => {
+        return {
+          trends: (data.trends ?? []).filter(t => {
+            return !hasMutedWord({
+              mutedWords,
+              text: t.topic + ' ' + t.displayName + ' ' + t.category,
+            })
+          }),
+        }
+      },
+      [mutedWords],
+    ),
+  })
+}
diff --git a/src/state/queries/useSuggestedStarterPacksQuery.ts b/src/state/queries/useSuggestedStarterPacksQuery.ts
new file mode 100644
index 000000000..18fe6439e
--- /dev/null
+++ b/src/state/queries/useSuggestedStarterPacksQuery.ts
@@ -0,0 +1,38 @@
+import {useQuery} from '@tanstack/react-query'
+
+import {
+  aggregateUserInterests,
+  createBskyTopicsHeader,
+} from '#/lib/api/feed/utils'
+import {getContentLanguages} from '#/state/preferences/languages'
+import {STALE} from '#/state/queries'
+import {usePreferencesQuery} from '#/state/queries/preferences'
+import {useAgent} from '#/state/session'
+
+export const createSuggestedStarterPacksQueryKey = () => [
+  'suggested-starter-packs',
+]
+
+export function useSuggestedStarterPacksQuery() {
+  const agent = useAgent()
+  const {data: preferences} = usePreferencesQuery()
+  const contentLangs = getContentLanguages().join(',')
+
+  return useQuery({
+    refetchOnWindowFocus: true,
+    staleTime: STALE.MINUTES.ONE,
+    queryKey: createSuggestedStarterPacksQueryKey(),
+    async queryFn() {
+      const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks(
+        undefined,
+        {
+          headers: {
+            ...createBskyTopicsHeader(aggregateUserInterests(preferences)),
+            'Accept-Language': contentLangs,
+          },
+        },
+      )
+      return data
+    },
+  })
+}
diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx
index a99077c0c..3a6b8f660 100644
--- a/src/view/com/posts/PostFeed.tsx
+++ b/src/view/com/posts/PostFeed.tsx
@@ -3,13 +3,13 @@ import {
   ActivityIndicator,
   AppState,
   Dimensions,
-  ListRenderItemInfo,
-  StyleProp,
+  type ListRenderItemInfo,
+  type StyleProp,
   StyleSheet,
   View,
-  ViewStyle,
+  type ViewStyle,
 } from 'react-native'
-import {AppBskyActorDefs, AppBskyEmbedVideo} from '@atproto/api'
+import {type AppBskyActorDefs, AppBskyEmbedVideo} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
@@ -24,21 +24,21 @@ import {useFeedFeedbackContext} from '#/state/feed-feedback'
 import {useTrendingSettings} from '#/state/preferences/trending'
 import {STALE} from '#/state/queries'
 import {
-  AuthorFilter,
-  FeedDescriptor,
-  FeedParams,
-  FeedPostSlice,
-  FeedPostSliceItem,
+  type AuthorFilter,
+  type FeedDescriptor,
+  type FeedParams,
+  type FeedPostSlice,
+  type FeedPostSliceItem,
   pollLatest,
   RQKEY,
   usePostFeedQuery,
 } from '#/state/queries/post-feed'
 import {useSession} from '#/state/session'
 import {useProgressGuide} from '#/state/shell/progress-guide'
-import {List, ListRef} from '#/view/com/util/List'
+import {List, type ListRef} from '#/view/com/util/List'
 import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
-import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
+import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
 import {useBreakpoints, useLayoutBreakpoints} from '#/alf'
 import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
 import {
@@ -767,14 +767,14 @@ const styles = StyleSheet.create({
   feedFooter: {paddingTop: 20},
 })
 
-function isThreadParentAt<T>(arr: Array<T>, i: number) {
+export function isThreadParentAt<T>(arr: Array<T>, i: number) {
   if (arr.length === 1) {
     return false
   }
   return i < arr.length - 1
 }
 
-function isThreadChildAt<T>(arr: Array<T>, i: number) {
+export function isThreadChildAt<T>(arr: Array<T>, i: number) {
   if (arr.length === 1) {
     return false
   }
diff --git a/src/view/com/util/numeric/format.ts b/src/view/com/util/numeric/format.ts
index 8f3ebd0e7..f05f28540 100644
--- a/src/view/com/util/numeric/format.ts
+++ b/src/view/com/util/numeric/format.ts
@@ -1,4 +1,4 @@
-import {I18n} from '@lingui/core'
+import {type I18n} from '@lingui/core'
 
 export const formatCount = (i18n: I18n, num: number) => {
   return i18n.number(num, {
diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx
deleted file mode 100644
index 520e103a4..000000000
--- a/src/view/screens/Search/Explore.tsx
+++ /dev/null
@@ -1,641 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {
-  AppBskyActorDefs,
-  AppBskyFeedDefs,
-  moderateProfile,
-  ModerationDecision,
-  ModerationOpts,
-} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {logEvent} from '#/lib/statsig/statsig'
-import {cleanError} from '#/lib/strings/errors'
-import {logger} from '#/logger'
-import {isNative, isWeb} from '#/platform/detection'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {useGetPopularFeedsQuery} from '#/state/queries/feed'
-import {usePreferencesQuery} from '#/state/queries/preferences'
-import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
-import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
-import {List} from '#/view/com/util/List'
-import {
-  FeedFeedLoadingPlaceholder,
-  ProfileCardFeedLoadingPlaceholder,
-} from '#/view/com/util/LoadingPlaceholder'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
-import {ExploreRecommendations} from '#/screens/Search/components/ExploreRecommendations'
-import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics'
-import {ExploreTrendingVideos} from '#/screens/Search/components/ExploreTrendingVideos'
-import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
-import {Button} from '#/components/Button'
-import * as FeedCard from '#/components/FeedCard'
-import {ArrowBottom_Stroke2_Corner0_Rounded as ArrowBottom} from '#/components/icons/Arrow'
-import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
-import {Props as SVGIconProps} from '#/components/icons/common'
-import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
-import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
-import {Loader} from '#/components/Loader'
-import {Text} from '#/components/Typography'
-
-function SuggestedItemsHeader({
-  title,
-  description,
-  style,
-  icon: Icon,
-}: {
-  title: string
-  description: string
-  icon: React.ComponentType<SVGIconProps>
-} & ViewStyleProp) {
-  const t = useTheme()
-
-  return (
-    <View
-      style={[
-        isWeb
-          ? [a.flex_row, a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
-          : [{flexDirection: 'row-reverse'}, a.p_lg, a.pt_2xl, a.gap_md],
-        a.border_b,
-        t.atoms.border_contrast_low,
-        style,
-      ]}>
-      <View style={[a.flex_1, a.gap_sm]}>
-        <View style={[a.flex_row, a.align_center, a.gap_sm]}>
-          <Icon
-            size="lg"
-            fill={t.palette.primary_500}
-            style={{marginLeft: -2}}
-          />
-          <Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>{title}</Text>
-        </View>
-        <Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
-          {description}
-        </Text>
-      </View>
-    </View>
-  )
-}
-
-type LoadMoreItem =
-  | {
-      type: 'profile'
-      key: string
-      avatar: string | undefined
-      moderation: ModerationDecision
-    }
-  | {
-      type: 'feed'
-      key: string
-      avatar: string | undefined
-      moderation: undefined
-    }
-
-function LoadMore({
-  item,
-  moderationOpts,
-}: {
-  item: ExploreScreenItems & {type: 'loadMore'}
-  moderationOpts?: ModerationOpts
-}) {
-  const t = useTheme()
-  const {_} = useLingui()
-  const items: LoadMoreItem[] = React.useMemo(() => {
-    return item.items
-      .map(_item => {
-        let loadMoreItem: LoadMoreItem | undefined
-        if (_item.type === 'profile') {
-          loadMoreItem = {
-            type: 'profile',
-            key: _item.profile.did,
-            avatar: _item.profile.avatar,
-            moderation: moderateProfile(_item.profile, moderationOpts!),
-          }
-        } else if (_item.type === 'feed') {
-          loadMoreItem = {
-            type: 'feed',
-            key: _item.feed.uri,
-            avatar: _item.feed.avatar,
-            moderation: undefined,
-          }
-        }
-        return loadMoreItem
-      })
-      .filter(n => !!n)
-  }, [item.items, moderationOpts])
-
-  if (items.length === 0) return null
-
-  const type = items[0].type
-
-  return (
-    <View style={[]}>
-      <Button
-        label={_(msg`Load more`)}
-        onPress={item.onLoadMore}
-        style={[a.relative, a.w_full]}>
-        {({hovered, pressed}) => (
-          <View
-            style={[
-              a.flex_1,
-              a.flex_row,
-              a.align_center,
-              a.px_lg,
-              a.py_md,
-              (hovered || pressed) && t.atoms.bg_contrast_25,
-            ]}>
-            <View
-              style={[
-                a.relative,
-                {
-                  height: 32,
-                  width: 32 + 15 * items.length,
-                },
-              ]}>
-              <View
-                style={[
-                  a.align_center,
-                  a.justify_center,
-                  t.atoms.bg_contrast_25,
-                  a.absolute,
-                  {
-                    width: 30,
-                    height: 30,
-                    left: 0,
-                    borderWidth: 1,
-                    backgroundColor: t.palette.primary_500,
-                    borderColor: t.atoms.bg.backgroundColor,
-                    borderRadius: type === 'profile' ? 999 : 4,
-                    zIndex: 4,
-                  },
-                ]}>
-                <ArrowBottom fill={t.palette.white} />
-              </View>
-              {items.map((_item, i) => {
-                return (
-                  <View
-                    key={_item.key}
-                    style={[
-                      t.atoms.bg_contrast_25,
-                      a.absolute,
-                      {
-                        width: 30,
-                        height: 30,
-                        left: (i + 1) * 15,
-                        borderWidth: 1,
-                        borderColor: t.atoms.bg.backgroundColor,
-                        borderRadius: _item.type === 'profile' ? 999 : 4,
-                        zIndex: 3 - i,
-                      },
-                    ]}>
-                    {moderationOpts && (
-                      <>
-                        {_item.type === 'profile' ? (
-                          <UserAvatar
-                            size={28}
-                            avatar={_item.avatar}
-                            moderation={_item.moderation.ui('avatar')}
-                            type="user"
-                          />
-                        ) : _item.type === 'feed' ? (
-                          <UserAvatar
-                            size={28}
-                            avatar={_item.avatar}
-                            type="algo"
-                          />
-                        ) : null}
-                      </>
-                    )}
-                  </View>
-                )
-              })}
-            </View>
-
-            <Text
-              style={[
-                a.pl_sm,
-                a.leading_snug,
-                hovered ? t.atoms.text : t.atoms.text_contrast_medium,
-              ]}>
-              {type === 'profile' ? (
-                <Trans>Load more suggested follows</Trans>
-              ) : (
-                <Trans>Load more suggested feeds</Trans>
-              )}
-            </Text>
-
-            <View style={[a.flex_1, a.align_end]}>
-              {item.isLoadingMore && <Loader size="lg" />}
-            </View>
-          </View>
-        )}
-      </Button>
-    </View>
-  )
-}
-
-type ExploreScreenItems =
-  | {
-      type: 'header'
-      key: string
-      title: string
-      description: string
-      style?: ViewStyleProp['style']
-      icon: React.ComponentType<SVGIconProps>
-    }
-  | {
-      type: 'trendingTopics'
-      key: string
-    }
-  | {
-      type: 'trendingVideos'
-      key: string
-    }
-  | {
-      type: 'recommendations'
-      key: string
-    }
-  | {
-      type: 'profile'
-      key: string
-      profile: AppBskyActorDefs.ProfileView
-      recId?: number
-    }
-  | {
-      type: 'feed'
-      key: string
-      feed: AppBskyFeedDefs.GeneratorView
-    }
-  | {
-      type: 'loadMore'
-      key: string
-      isLoadingMore: boolean
-      onLoadMore: () => void
-      items: ExploreScreenItems[]
-    }
-  | {
-      type: 'profilePlaceholder'
-      key: string
-    }
-  | {
-      type: 'feedPlaceholder'
-      key: string
-    }
-  | {
-      type: 'error'
-      key: string
-      message: string
-      error: string
-    }
-
-export function Explore() {
-  const {_} = useLingui()
-  const t = useTheme()
-  const {data: preferences, error: preferencesError} = usePreferencesQuery()
-  const moderationOpts = useModerationOpts()
-  const {
-    data: profiles,
-    hasNextPage: hasNextProfilesPage,
-    isLoading: isLoadingProfiles,
-    isFetchingNextPage: isFetchingNextProfilesPage,
-    error: profilesError,
-    fetchNextPage: fetchNextProfilesPage,
-  } = useSuggestedFollowsQuery({limit: 6, subsequentPageLimit: 10})
-  const {
-    data: feeds,
-    hasNextPage: hasNextFeedsPage,
-    isLoading: isLoadingFeeds,
-    isFetchingNextPage: isFetchingNextFeedsPage,
-    error: feedsError,
-    fetchNextPage: fetchNextFeedsPage,
-  } = useGetPopularFeedsQuery({limit: 10})
-
-  const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
-  const onLoadMoreProfiles = React.useCallback(async () => {
-    if (isFetchingNextProfilesPage || !hasNextProfilesPage || profilesError)
-      return
-    try {
-      await fetchNextProfilesPage()
-    } catch (err) {
-      logger.error('Failed to load more suggested follows', {message: err})
-    }
-  }, [
-    isFetchingNextProfilesPage,
-    hasNextProfilesPage,
-    profilesError,
-    fetchNextProfilesPage,
-  ])
-
-  const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
-  const onLoadMoreFeeds = React.useCallback(async () => {
-    if (isFetchingNextFeedsPage || !hasNextFeedsPage || feedsError) return
-    try {
-      await fetchNextFeedsPage()
-    } catch (err) {
-      logger.error('Failed to load more suggested follows', {message: err})
-    }
-  }, [
-    isFetchingNextFeedsPage,
-    hasNextFeedsPage,
-    feedsError,
-    fetchNextFeedsPage,
-  ])
-
-  const items = React.useMemo<ExploreScreenItems[]>(() => {
-    const i: ExploreScreenItems[] = []
-
-    i.push({
-      type: 'trendingTopics',
-      key: `trending-topics`,
-    })
-
-    if (isNative) {
-      i.push({
-        type: 'trendingVideos',
-        key: `trending-videos`,
-      })
-    }
-
-    i.push({
-      type: 'recommendations',
-      key: `recommendations`,
-    })
-
-    i.push({
-      type: 'header',
-      key: 'suggested-follows-header',
-      title: _(msg`Suggested accounts`),
-      description: _(
-        msg`Follow more accounts to get connected to your interests and build your network.`,
-      ),
-      icon: Person,
-    })
-
-    if (profiles) {
-      // Currently the responses contain duplicate items.
-      // Needs to be fixed on backend, but let's dedupe to be safe.
-      let seen = new Set()
-      const profileItems: ExploreScreenItems[] = []
-      for (const page of profiles.pages) {
-        for (const actor of page.actors) {
-          if (!seen.has(actor.did)) {
-            seen.add(actor.did)
-            profileItems.push({
-              type: 'profile',
-              key: actor.did,
-              profile: actor,
-              recId: page.recId,
-            })
-          }
-        }
-      }
-
-      if (hasNextProfilesPage) {
-        // splice off 3 as previews if we have a next page
-        const previews = profileItems.splice(-3)
-        // push remainder
-        i.push(...profileItems)
-        i.push({
-          type: 'loadMore',
-          key: 'loadMoreProfiles',
-          isLoadingMore: isLoadingMoreProfiles,
-          onLoadMore: onLoadMoreProfiles,
-          items: previews,
-        })
-      } else {
-        i.push(...profileItems)
-      }
-    } else {
-      if (profilesError) {
-        i.push({
-          type: 'error',
-          key: 'profilesError',
-          message: _(msg`Failed to load suggested follows`),
-          error: cleanError(profilesError),
-        })
-      } else {
-        i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
-      }
-    }
-
-    i.push({
-      type: 'header',
-      key: 'suggested-feeds-header',
-      title: _(msg`Discover new feeds`),
-      description: _(
-        msg`Choose your own timeline! Feeds built by the community help you find content you love.`,
-      ),
-      style: [a.pt_5xl],
-      icon: ListSparkle,
-    })
-
-    if (feeds && preferences) {
-      // Currently the responses contain duplicate items.
-      // Needs to be fixed on backend, but let's dedupe to be safe.
-      let seen = new Set()
-      const feedItems: ExploreScreenItems[] = []
-      for (const page of feeds.pages) {
-        for (const feed of page.feeds) {
-          if (!seen.has(feed.uri)) {
-            seen.add(feed.uri)
-            feedItems.push({
-              type: 'feed',
-              key: feed.uri,
-              feed,
-            })
-          }
-        }
-      }
-
-      // feeds errors can occur during pagination, so feeds is truthy
-      if (feedsError) {
-        i.push({
-          type: 'error',
-          key: 'feedsError',
-          message: _(msg`Failed to load suggested feeds`),
-          error: cleanError(feedsError),
-        })
-      } else if (preferencesError) {
-        i.push({
-          type: 'error',
-          key: 'preferencesError',
-          message: _(msg`Failed to load feeds preferences`),
-          error: cleanError(preferencesError),
-        })
-      } else if (hasNextFeedsPage) {
-        const preview = feedItems.splice(-3)
-        i.push(...feedItems)
-        i.push({
-          type: 'loadMore',
-          key: 'loadMoreFeeds',
-          isLoadingMore: isLoadingMoreFeeds,
-          onLoadMore: onLoadMoreFeeds,
-          items: preview,
-        })
-      } else {
-        i.push(...feedItems)
-      }
-    } else {
-      if (feedsError) {
-        i.push({
-          type: 'error',
-          key: 'feedsError',
-          message: _(msg`Failed to load suggested feeds`),
-          error: cleanError(feedsError),
-        })
-      } else if (preferencesError) {
-        i.push({
-          type: 'error',
-          key: 'preferencesError',
-          message: _(msg`Failed to load feeds preferences`),
-          error: cleanError(preferencesError),
-        })
-      } else {
-        i.push({type: 'feedPlaceholder', key: 'feedPlaceholder'})
-      }
-    }
-
-    return i
-  }, [
-    _,
-    profiles,
-    feeds,
-    preferences,
-    onLoadMoreFeeds,
-    onLoadMoreProfiles,
-    isLoadingMoreProfiles,
-    isLoadingMoreFeeds,
-    profilesError,
-    feedsError,
-    preferencesError,
-    hasNextProfilesPage,
-    hasNextFeedsPage,
-  ])
-
-  const renderItem = React.useCallback(
-    ({item, index}: {item: ExploreScreenItems; index: number}) => {
-      switch (item.type) {
-        case 'header': {
-          return (
-            <SuggestedItemsHeader
-              title={item.title}
-              description={item.description}
-              style={item.style}
-              icon={item.icon}
-            />
-          )
-        }
-        case 'trendingTopics': {
-          return <ExploreTrendingTopics />
-        }
-        case 'trendingVideos': {
-          return <ExploreTrendingVideos />
-        }
-        case 'recommendations': {
-          return <ExploreRecommendations />
-        }
-        case 'profile': {
-          return (
-            <View style={[a.border_b, t.atoms.border_contrast_low]}>
-              <ProfileCardWithFollowBtn
-                profile={item.profile}
-                noBg
-                noBorder
-                showKnownFollowers
-                onPress={() => {
-                  logEvent('suggestedUser:press', {
-                    logContext: 'Explore',
-                    recId: item.recId,
-                    position: index,
-                  })
-                }}
-                onFollow={() => {
-                  logEvent('suggestedUser:follow', {
-                    logContext: 'Explore',
-                    location: 'Card',
-                    recId: item.recId,
-                    position: index,
-                  })
-                }}
-              />
-            </View>
-          )
-        }
-        case 'feed': {
-          return (
-            <View
-              style={[
-                a.border_b,
-                t.atoms.border_contrast_low,
-                a.px_lg,
-                a.py_lg,
-              ]}>
-              <FeedCard.Default view={item.feed} />
-            </View>
-          )
-        }
-        case 'loadMore': {
-          return <LoadMore item={item} moderationOpts={moderationOpts} />
-        }
-        case 'profilePlaceholder': {
-          return <ProfileCardFeedLoadingPlaceholder />
-        }
-        case 'feedPlaceholder': {
-          return <FeedFeedLoadingPlaceholder />
-        }
-        case 'error': {
-          return (
-            <View
-              style={[
-                a.border_t,
-                a.pt_md,
-                a.px_md,
-                t.atoms.border_contrast_low,
-              ]}>
-              <View
-                style={[
-                  a.flex_row,
-                  a.gap_md,
-                  a.p_lg,
-                  a.rounded_sm,
-                  t.atoms.bg_contrast_25,
-                ]}>
-                <CircleInfo size="md" fill={t.palette.negative_400} />
-                <View style={[a.flex_1, a.gap_sm]}>
-                  <Text style={[a.font_bold, a.leading_snug]}>
-                    {item.message}
-                  </Text>
-                  <Text
-                    style={[
-                      a.italic,
-                      a.leading_snug,
-                      t.atoms.text_contrast_medium,
-                    ]}>
-                    {item.error}
-                  </Text>
-                </View>
-              </View>
-            </View>
-          )
-        }
-      }
-    },
-    [t, moderationOpts],
-  )
-
-  // note: actually not a screen, instead it's nested within
-  // the search screen. so we don't need Layout.Screen
-  return (
-    <List
-      data={items}
-      renderItem={renderItem}
-      keyExtractor={item => item.key}
-      // @ts-ignore web only -prf
-      desktopFixedHeight
-      contentContainerStyle={{paddingBottom: 100}}
-      keyboardShouldPersistTaps="handled"
-      keyboardDismissMode="on-drag"
-    />
-  )
-}
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
deleted file mode 100644
index 785e1872e..000000000
--- a/src/view/screens/Search/Search.tsx
+++ /dev/null
@@ -1,1165 +0,0 @@
-import React, {useCallback, useLayoutEffect, useMemo} from 'react'
-import {
-  ActivityIndicator,
-  Pressable,
-  StyleProp,
-  StyleSheet,
-  TextInput,
-  View,
-  ViewStyle,
-} from 'react-native'
-import {ScrollView as RNGHScrollView} from 'react-native-gesture-handler'
-import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native'
-import {useQueryClient} from '@tanstack/react-query'
-
-import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
-import {createHitslop, HITSLOP_20} from '#/lib/constants'
-import {HITSLOP_10} from '#/lib/constants'
-import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
-import {MagnifyingGlassIcon} from '#/lib/icons'
-import {makeProfileLink} from '#/lib/routes/links'
-import {NavigationProp} from '#/lib/routes/types'
-import {
-  NativeStackScreenProps,
-  SearchTabNavigatorParams,
-} from '#/lib/routes/types'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {augmentSearchQuery} from '#/lib/strings/helpers'
-import {languageName} from '#/locale/helpers'
-import {isNative, isWeb} from '#/platform/detection'
-import {listenSoftReset} from '#/state/events'
-import {useLanguagePrefs} from '#/state/preferences/languages'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
-import {useActorSearch} from '#/state/queries/actor-search'
-import {usePopularFeedsSearch} from '#/state/queries/feed'
-import {
-  unstableCacheProfileView,
-  useProfilesQuery,
-} from '#/state/queries/profile'
-import {useSearchPostsQuery} from '#/state/queries/search-posts'
-import {useSession} from '#/state/session'
-import {useSetMinimalShellMode} from '#/state/shell'
-import {Pager} from '#/view/com/pager/Pager'
-import {TabBar} from '#/view/com/pager/TabBar'
-import {Post} from '#/view/com/post/Post'
-import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
-import {Link} from '#/view/com/util/Link'
-import {List} from '#/view/com/util/List'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
-import {Explore} from '#/view/screens/Search/Explore'
-import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
-import {makeSearchQuery, Params, parseSearchQuery} from '#/screens/Search/utils'
-import {
-  atoms as a,
-  native,
-  platform,
-  tokens,
-  useBreakpoints,
-  useTheme,
-  web,
-} from '#/alf'
-import {Button, ButtonIcon, ButtonText} from '#/components/Button'
-import * as FeedCard from '#/components/FeedCard'
-import {SearchInput} from '#/components/forms/SearchInput'
-import {
-  ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
-  ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon,
-} from '#/components/icons/Chevron'
-import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe'
-import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
-import * as Layout from '#/components/Layout'
-import * as Menu from '#/components/Menu'
-import {Text} from '#/components/Typography'
-import {account, useStorage} from '#/storage'
-import * as bsky from '#/types/bsky'
-
-function Loader() {
-  return (
-    <Layout.Content>
-      <View style={[a.py_xl]}>
-        <ActivityIndicator />
-      </View>
-    </Layout.Content>
-  )
-}
-
-function EmptyState({message, error}: {message: string; error?: string}) {
-  const t = useTheme()
-
-  return (
-    <Layout.Content>
-      <View style={[a.p_xl]}>
-        <View style={[t.atoms.bg_contrast_25, a.rounded_sm, a.p_lg]}>
-          <Text style={[a.text_md]}>{message}</Text>
-
-          {error && (
-            <>
-              <View
-                style={[
-                  {
-                    marginVertical: 12,
-                    height: 1,
-                    width: '100%',
-                    backgroundColor: t.atoms.text.color,
-                    opacity: 0.2,
-                  },
-                ]}
-              />
-
-              <Text style={[t.atoms.text_contrast_medium]}>
-                <Trans>Error:</Trans> {error}
-              </Text>
-            </>
-          )}
-        </View>
-      </View>
-    </Layout.Content>
-  )
-}
-
-type SearchResultSlice =
-  | {
-      type: 'post'
-      key: string
-      post: AppBskyFeedDefs.PostView
-    }
-  | {
-      type: 'loadingMore'
-      key: string
-    }
-
-let SearchScreenPostResults = ({
-  query,
-  sort,
-  active,
-}: {
-  query: string
-  sort?: 'top' | 'latest'
-  active: boolean
-}): React.ReactNode => {
-  const {_} = useLingui()
-  const {currentAccount} = useSession()
-  const [isPTR, setIsPTR] = React.useState(false)
-
-  const augmentedQuery = React.useMemo(() => {
-    return augmentSearchQuery(query || '', {did: currentAccount?.did})
-  }, [query, currentAccount])
-
-  const {
-    isFetched,
-    data: results,
-    isFetching,
-    error,
-    refetch,
-    fetchNextPage,
-    isFetchingNextPage,
-    hasNextPage,
-  } = useSearchPostsQuery({query: augmentedQuery, sort, enabled: active})
-
-  const onPullToRefresh = React.useCallback(async () => {
-    setIsPTR(true)
-    await refetch()
-    setIsPTR(false)
-  }, [setIsPTR, refetch])
-  const onEndReached = React.useCallback(() => {
-    if (isFetching || !hasNextPage || error) return
-    fetchNextPage()
-  }, [isFetching, error, hasNextPage, fetchNextPage])
-
-  const posts = React.useMemo(() => {
-    return results?.pages.flatMap(page => page.posts) || []
-  }, [results])
-  const items = React.useMemo(() => {
-    let temp: SearchResultSlice[] = []
-
-    const seenUris = new Set()
-    for (const post of posts) {
-      if (seenUris.has(post.uri)) {
-        continue
-      }
-      temp.push({
-        type: 'post',
-        key: post.uri,
-        post,
-      })
-      seenUris.add(post.uri)
-    }
-
-    if (isFetchingNextPage) {
-      temp.push({
-        type: 'loadingMore',
-        key: 'loadingMore',
-      })
-    }
-
-    return temp
-  }, [posts, isFetchingNextPage])
-
-  return error ? (
-    <EmptyState
-      message={_(
-        msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`,
-      )}
-      error={error.toString()}
-    />
-  ) : (
-    <>
-      {isFetched ? (
-        <>
-          {posts.length ? (
-            <List
-              data={items}
-              renderItem={({item}) => {
-                if (item.type === 'post') {
-                  return <Post post={item.post} />
-                } else {
-                  return null
-                }
-              }}
-              keyExtractor={item => item.key}
-              refreshing={isPTR}
-              onRefresh={onPullToRefresh}
-              onEndReached={onEndReached}
-              desktopFixedHeight
-              contentContainerStyle={{paddingBottom: 100}}
-            />
-          ) : (
-            <EmptyState message={_(msg`No results found for ${query}`)} />
-          )}
-        </>
-      ) : (
-        <Loader />
-      )}
-    </>
-  )
-}
-SearchScreenPostResults = React.memo(SearchScreenPostResults)
-
-let SearchScreenUserResults = ({
-  query,
-  active,
-}: {
-  query: string
-  active: boolean
-}): React.ReactNode => {
-  const {_} = useLingui()
-
-  const {data: results, isFetched} = useActorSearch({
-    query,
-    enabled: active,
-  })
-
-  return isFetched && results ? (
-    <>
-      {results.length ? (
-        <List
-          data={results}
-          renderItem={({item}) => (
-            <ProfileCardWithFollowBtn profile={item} noBg />
-          )}
-          keyExtractor={item => item.did}
-          desktopFixedHeight
-          contentContainerStyle={{paddingBottom: 100}}
-        />
-      ) : (
-        <EmptyState message={_(msg`No results found for ${query}`)} />
-      )}
-    </>
-  ) : (
-    <Loader />
-  )
-}
-SearchScreenUserResults = React.memo(SearchScreenUserResults)
-
-let SearchScreenFeedsResults = ({
-  query,
-  active,
-}: {
-  query: string
-  active: boolean
-}): React.ReactNode => {
-  const t = useTheme()
-  const {_} = useLingui()
-
-  const {data: results, isFetched} = usePopularFeedsSearch({
-    query,
-    enabled: active,
-  })
-
-  return isFetched && results ? (
-    <>
-      {results.length ? (
-        <List
-          data={results}
-          renderItem={({item}) => (
-            <View
-              style={[
-                a.border_b,
-                t.atoms.border_contrast_low,
-                a.px_lg,
-                a.py_lg,
-              ]}>
-              <FeedCard.Default view={item} />
-            </View>
-          )}
-          keyExtractor={item => item.uri}
-          desktopFixedHeight
-          contentContainerStyle={{paddingBottom: 100}}
-        />
-      ) : (
-        <EmptyState message={_(msg`No results found for ${query}`)} />
-      )}
-    </>
-  ) : (
-    <Loader />
-  )
-}
-SearchScreenFeedsResults = React.memo(SearchScreenFeedsResults)
-
-function SearchLanguageDropdown({
-  value,
-  onChange,
-}: {
-  value: string
-  onChange(value: string): void
-}) {
-  const {_} = useLingui()
-  const {appLanguage, contentLanguages} = useLanguagePrefs()
-
-  const languages = useMemo(() => {
-    return LANGUAGES.filter(
-      (lang, index, self) =>
-        Boolean(lang.code2) && // reduce to the code2 varieties
-        index === self.findIndex(t => t.code2 === lang.code2), // remove dupes (which will happen)
-    )
-      .map(l => ({
-        label: languageName(l, appLanguage),
-        value: l.code2,
-        key: l.code2 + l.code3,
-      }))
-      .sort((a, b) => {
-        // prioritize user's languages
-        const aIsUser = contentLanguages.includes(a.value)
-        const bIsUser = contentLanguages.includes(b.value)
-        if (aIsUser && !bIsUser) return -1
-        if (bIsUser && !aIsUser) return 1
-        // prioritize "common" langs in the network
-        const aIsCommon = !!APP_LANGUAGES.find(
-          al =>
-            // skip `ast`, because it uses a 3-letter code which conflicts with `as`
-            // it begins with `a` anyway so still is top of the list
-            al.code2 !== 'ast' && al.code2.startsWith(a.value),
-        )
-        const bIsCommon = !!APP_LANGUAGES.find(
-          al =>
-            // ditto
-            al.code2 !== 'ast' && al.code2.startsWith(b.value),
-        )
-        if (aIsCommon && !bIsCommon) return -1
-        if (bIsCommon && !aIsCommon) return 1
-        // fall back to alphabetical
-        return a.label.localeCompare(b.label)
-      })
-  }, [appLanguage, contentLanguages])
-
-  const currentLanguageLabel =
-    languages.find(lang => lang.value === value)?.label ?? _(msg`All languages`)
-
-  return (
-    <Menu.Root>
-      <Menu.Trigger
-        label={_(
-          msg`Filter search by language (currently: ${currentLanguageLabel})`,
-        )}>
-        {({props}) => (
-          <Button
-            {...props}
-            label={props.accessibilityLabel}
-            size="small"
-            color={platform({native: 'primary', default: 'secondary'})}
-            variant={platform({native: 'ghost', default: 'solid'})}
-            style={native([
-              a.py_sm,
-              a.px_sm,
-              {marginRight: tokens.space.sm * -1},
-            ])}>
-            <ButtonIcon icon={EarthIcon} />
-            <ButtonText>{currentLanguageLabel}</ButtonText>
-            <ButtonIcon
-              icon={platform({
-                native: ChevronUpDownIcon,
-                default: ChevronDownIcon,
-              })}
-            />
-          </Button>
-        )}
-      </Menu.Trigger>
-      <Menu.Outer>
-        <Menu.LabelText>
-          <Trans>Filter search by language</Trans>
-        </Menu.LabelText>
-        <Menu.Item label={_(msg`All languages`)} onPress={() => onChange('')}>
-          <Menu.ItemText>
-            <Trans>All languages</Trans>
-          </Menu.ItemText>
-          <Menu.ItemRadio selected={value === ''} />
-        </Menu.Item>
-        <Menu.Divider />
-        <Menu.Group>
-          {languages.map(lang => (
-            <Menu.Item
-              key={lang.key}
-              label={lang.label}
-              onPress={() => onChange(lang.value)}>
-              <Menu.ItemText>{lang.label}</Menu.ItemText>
-              <Menu.ItemRadio selected={value === lang.value} />
-            </Menu.Item>
-          ))}
-        </Menu.Group>
-      </Menu.Outer>
-    </Menu.Root>
-  )
-}
-
-function useQueryManager({
-  initialQuery,
-  fixedParams,
-}: {
-  initialQuery: string
-  fixedParams?: Params
-}) {
-  const {query, params: initialParams} = React.useMemo(() => {
-    return parseSearchQuery(initialQuery || '')
-  }, [initialQuery])
-  const [prevInitialQuery, setPrevInitialQuery] = React.useState(initialQuery)
-  const [lang, setLang] = React.useState(initialParams.lang || '')
-
-  if (initialQuery !== prevInitialQuery) {
-    // handle new queryParam change (from manual search entry)
-    setPrevInitialQuery(initialQuery)
-    setLang(initialParams.lang || '')
-  }
-
-  const params = React.useMemo(
-    () => ({
-      // default stuff
-      ...initialParams,
-      // managed stuff
-      lang,
-      ...fixedParams,
-    }),
-    [lang, initialParams, fixedParams],
-  )
-  const handlers = React.useMemo(
-    () => ({
-      setLang,
-    }),
-    [setLang],
-  )
-
-  return React.useMemo(() => {
-    return {
-      query,
-      queryWithParams: makeSearchQuery(query, params),
-      params: {
-        ...params,
-        ...handlers,
-      },
-    }
-  }, [query, params, handlers])
-}
-
-let SearchScreenInner = ({
-  query,
-  queryWithParams,
-  headerHeight,
-}: {
-  query: string
-  queryWithParams: string
-  headerHeight: number
-}): React.ReactNode => {
-  const t = useTheme()
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {hasSession} = useSession()
-  const {gtTablet} = useBreakpoints()
-  const [activeTab, setActiveTab] = React.useState(0)
-  const {_} = useLingui()
-
-  const onPageSelected = React.useCallback(
-    (index: number) => {
-      setMinimalShellMode(false)
-      setActiveTab(index)
-    },
-    [setMinimalShellMode],
-  )
-
-  const sections = React.useMemo(() => {
-    if (!queryWithParams) return []
-    const noParams = queryWithParams === query
-    return [
-      {
-        title: _(msg`Top`),
-        component: (
-          <SearchScreenPostResults
-            query={queryWithParams}
-            sort="top"
-            active={activeTab === 0}
-          />
-        ),
-      },
-      {
-        title: _(msg`Latest`),
-        component: (
-          <SearchScreenPostResults
-            query={queryWithParams}
-            sort="latest"
-            active={activeTab === 1}
-          />
-        ),
-      },
-      noParams && {
-        title: _(msg`People`),
-        component: (
-          <SearchScreenUserResults query={query} active={activeTab === 2} />
-        ),
-      },
-      noParams && {
-        title: _(msg`Feeds`),
-        component: (
-          <SearchScreenFeedsResults query={query} active={activeTab === 3} />
-        ),
-      },
-    ].filter(Boolean) as {
-      title: string
-      component: React.ReactNode
-    }[]
-  }, [_, query, queryWithParams, activeTab])
-
-  return queryWithParams ? (
-    <Pager
-      onPageSelected={onPageSelected}
-      renderTabBar={props => (
-        <Layout.Center style={[a.z_10, web([a.sticky, {top: headerHeight}])]}>
-          <TabBar items={sections.map(section => section.title)} {...props} />
-        </Layout.Center>
-      )}
-      initialPage={0}>
-      {sections.map((section, i) => (
-        <View key={i}>{section.component}</View>
-      ))}
-    </Pager>
-  ) : hasSession ? (
-    <Explore />
-  ) : (
-    <Layout.Center>
-      <View style={a.flex_1}>
-        {gtTablet && (
-          <View
-            style={[
-              a.border_b,
-              t.atoms.border_contrast_low,
-              a.px_lg,
-              a.pt_sm,
-              a.pb_lg,
-            ]}>
-            <Text style={[a.text_2xl, a.font_heavy]}>
-              <Trans>Search</Trans>
-            </Text>
-          </View>
-        )}
-
-        <View style={[a.align_center, a.justify_center, a.py_4xl, a.gap_lg]}>
-          <MagnifyingGlassIcon
-            strokeWidth={3}
-            size={60}
-            style={t.atoms.text_contrast_medium as StyleProp<ViewStyle>}
-          />
-          <Text style={[t.atoms.text_contrast_medium, a.text_md]}>
-            <Trans>Find posts, users, and feeds on Bluesky</Trans>
-          </Text>
-        </View>
-      </View>
-    </Layout.Center>
-  )
-}
-SearchScreenInner = React.memo(SearchScreenInner)
-
-export function SearchScreen(
-  props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
-) {
-  const queryParam = props.route?.params?.q ?? ''
-
-  return <SearchScreenShell queryParam={queryParam} testID="searchScreen" />
-}
-
-export function SearchScreenShell({
-  queryParam,
-  testID,
-  fixedParams,
-  navButton = 'menu',
-  inputPlaceholder,
-}: {
-  queryParam: string
-  testID: string
-  fixedParams?: Params
-  navButton?: 'back' | 'menu'
-  inputPlaceholder?: string
-}) {
-  const t = useTheme()
-  const {gtMobile} = useBreakpoints()
-  const navigation = useNavigation<NavigationProp>()
-  const route = useRoute()
-  const textInput = React.useRef<TextInput>(null)
-  const {_} = useLingui()
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {currentAccount} = useSession()
-  const queryClient = useQueryClient()
-
-  // Query terms
-  const [searchText, setSearchText] = React.useState<string>(queryParam)
-  const {data: autocompleteData, isFetching: isAutocompleteFetching} =
-    useActorAutocompleteQuery(searchText, true)
-
-  const [showAutocomplete, setShowAutocomplete] = React.useState(false)
-
-  const [termHistory = [], setTermHistory] = useStorage(account, [
-    currentAccount?.did ?? 'pwi',
-    'searchTermHistory',
-  ] as const)
-  const [accountHistory = [], setAccountHistory] = useStorage(account, [
-    currentAccount?.did ?? 'pwi',
-    'searchAccountHistory',
-  ])
-
-  const {data: accountHistoryProfiles} = useProfilesQuery({
-    handles: accountHistory,
-    maintainData: true,
-  })
-
-  const updateSearchHistory = useCallback(
-    async (item: string) => {
-      if (!item) return
-      const newSearchHistory = [
-        item,
-        ...termHistory.filter(search => search !== item),
-      ].slice(0, 6)
-      setTermHistory(newSearchHistory)
-    },
-    [termHistory, setTermHistory],
-  )
-
-  const updateProfileHistory = useCallback(
-    async (item: bsky.profile.AnyProfileView) => {
-      const newAccountHistory = [
-        item.did,
-        ...accountHistory.filter(p => p !== item.did),
-      ].slice(0, 5)
-      setAccountHistory(newAccountHistory)
-    },
-    [accountHistory, setAccountHistory],
-  )
-
-  const deleteSearchHistoryItem = useCallback(
-    async (item: string) => {
-      setTermHistory(termHistory.filter(search => search !== item))
-    },
-    [termHistory, setTermHistory],
-  )
-  const deleteProfileHistoryItem = useCallback(
-    async (item: AppBskyActorDefs.ProfileViewDetailed) => {
-      setAccountHistory(accountHistory.filter(p => p !== item.did))
-    },
-    [accountHistory, setAccountHistory],
-  )
-
-  const {params, query, queryWithParams} = useQueryManager({
-    initialQuery: queryParam,
-    fixedParams,
-  })
-  const showFilters = Boolean(queryWithParams && !showAutocomplete)
-
-  // web only - measure header height for sticky positioning
-  const [headerHeight, setHeaderHeight] = React.useState(0)
-  const headerRef = React.useRef(null)
-  useLayoutEffect(() => {
-    if (isWeb) {
-      if (!headerRef.current) return
-      const measurement = (headerRef.current as Element).getBoundingClientRect()
-      setHeaderHeight(measurement.height)
-    }
-  }, [])
-
-  useFocusEffect(
-    useNonReactiveCallback(() => {
-      if (isWeb) {
-        setSearchText(queryParam)
-      }
-    }),
-  )
-
-  const onPressClearQuery = React.useCallback(() => {
-    scrollToTopWeb()
-    setSearchText('')
-    textInput.current?.focus()
-  }, [])
-
-  const onChangeText = React.useCallback(async (text: string) => {
-    scrollToTopWeb()
-    setSearchText(text)
-  }, [])
-
-  const navigateToItem = React.useCallback(
-    (item: string) => {
-      scrollToTopWeb()
-      setShowAutocomplete(false)
-      updateSearchHistory(item)
-
-      if (isWeb) {
-        // @ts-expect-error route is not typesafe
-        navigation.push(route.name, {...route.params, q: item})
-      } else {
-        textInput.current?.blur()
-        navigation.setParams({q: item})
-      }
-    },
-    [updateSearchHistory, navigation, route],
-  )
-
-  const onPressCancelSearch = React.useCallback(() => {
-    scrollToTopWeb()
-    textInput.current?.blur()
-    setShowAutocomplete(false)
-    if (isWeb) {
-      // Empty params resets the URL to be /search rather than /search?q=
-      // eslint-disable-next-line @typescript-eslint/no-unused-vars
-      const {q: _q, ...parameters} = (route.params ?? {}) as {
-        [key: string]: string
-      }
-      // @ts-expect-error route is not typesafe
-      navigation.replace(route.name, parameters)
-    } else {
-      setSearchText('')
-      navigation.setParams({q: ''})
-    }
-  }, [setShowAutocomplete, setSearchText, navigation, route.params, route.name])
-
-  const onSubmit = React.useCallback(() => {
-    navigateToItem(searchText)
-  }, [navigateToItem, searchText])
-
-  const onAutocompleteResultPress = React.useCallback(() => {
-    if (isWeb) {
-      setShowAutocomplete(false)
-    } else {
-      textInput.current?.blur()
-    }
-  }, [])
-
-  const handleHistoryItemClick = React.useCallback(
-    (item: string) => {
-      setSearchText(item)
-      navigateToItem(item)
-    },
-    [navigateToItem],
-  )
-
-  const handleProfileClick = React.useCallback(
-    (profile: bsky.profile.AnyProfileView) => {
-      unstableCacheProfileView(queryClient, profile)
-      // Slight delay to avoid updating during push nav animation.
-      setTimeout(() => {
-        updateProfileHistory(profile)
-      }, 400)
-    },
-    [updateProfileHistory, queryClient],
-  )
-
-  const onSoftReset = React.useCallback(() => {
-    if (isWeb) {
-      // Empty params resets the URL to be /search rather than /search?q=
-      // eslint-disable-next-line @typescript-eslint/no-unused-vars
-      const {q: _q, ...parameters} = (route.params ?? {}) as {
-        [key: string]: string
-      }
-      // @ts-expect-error route is not typesafe
-      navigation.replace(route.name, parameters)
-    } else {
-      setSearchText('')
-      navigation.setParams({q: ''})
-      textInput.current?.focus()
-    }
-  }, [navigation, route])
-
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-      return listenSoftReset(onSoftReset)
-    }, [onSoftReset, setMinimalShellMode]),
-  )
-
-  const onSearchInputFocus = React.useCallback(() => {
-    if (isWeb) {
-      // Prevent a jump on iPad by ensuring that
-      // the initial focused render has no result list.
-      requestAnimationFrame(() => {
-        setShowAutocomplete(true)
-      })
-    } else {
-      setShowAutocomplete(true)
-    }
-  }, [setShowAutocomplete])
-
-  const showHeader = !gtMobile || navButton !== 'menu'
-
-  return (
-    <Layout.Screen testID={testID}>
-      <View
-        ref={headerRef}
-        onLayout={evt => {
-          if (isWeb) setHeaderHeight(evt.nativeEvent.layout.height)
-        }}
-        style={[
-          a.relative,
-          a.z_10,
-          web({
-            position: 'sticky',
-            top: 0,
-          }),
-        ]}>
-        <Layout.Center style={t.atoms.bg}>
-          {showHeader && (
-            <View
-              // HACK: shift up search input. we can't remove the top padding
-              // on the search input because it messes up the layout animation
-              // if we add it only when the header is hidden
-              style={{marginBottom: tokens.space.xs * -1}}>
-              <Layout.Header.Outer noBottomBorder>
-                {navButton === 'menu' ? (
-                  <Layout.Header.MenuButton />
-                ) : (
-                  <Layout.Header.BackButton />
-                )}
-                <Layout.Header.Content align="left">
-                  <Layout.Header.TitleText>
-                    <Trans>Search</Trans>
-                  </Layout.Header.TitleText>
-                </Layout.Header.Content>
-                {showFilters ? (
-                  <SearchLanguageDropdown
-                    value={params.lang}
-                    onChange={params.setLang}
-                  />
-                ) : (
-                  <Layout.Header.Slot />
-                )}
-              </Layout.Header.Outer>
-            </View>
-          )}
-          <View style={[a.px_md, a.pt_sm, a.pb_sm, a.overflow_hidden]}>
-            <View style={[a.gap_sm]}>
-              <View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs]}>
-                <View style={[a.flex_1]}>
-                  <SearchInput
-                    ref={textInput}
-                    value={searchText}
-                    onFocus={onSearchInputFocus}
-                    onChangeText={onChangeText}
-                    onClearText={onPressClearQuery}
-                    onSubmitEditing={onSubmit}
-                    placeholder={
-                      inputPlaceholder ??
-                      _(msg`Search for posts, users, or feeds`)
-                    }
-                    hitSlop={{...HITSLOP_20, top: 0}}
-                  />
-                </View>
-                {showAutocomplete && (
-                  <Button
-                    label={_(msg`Cancel search`)}
-                    size="large"
-                    variant="ghost"
-                    color="secondary"
-                    style={[a.px_sm]}
-                    onPress={onPressCancelSearch}
-                    hitSlop={HITSLOP_10}>
-                    <ButtonText>
-                      <Trans>Cancel</Trans>
-                    </ButtonText>
-                  </Button>
-                )}
-              </View>
-
-              {showFilters && !showHeader && (
-                <View
-                  style={[
-                    a.flex_row,
-                    a.align_center,
-                    a.justify_between,
-                    a.gap_sm,
-                  ]}>
-                  <SearchLanguageDropdown
-                    value={params.lang}
-                    onChange={params.setLang}
-                  />
-                </View>
-              )}
-            </View>
-          </View>
-        </Layout.Center>
-      </View>
-
-      <View
-        style={{
-          display: showAutocomplete && !fixedParams ? 'flex' : 'none',
-          flex: 1,
-        }}>
-        {searchText.length > 0 ? (
-          <AutocompleteResults
-            isAutocompleteFetching={isAutocompleteFetching}
-            autocompleteData={autocompleteData}
-            searchText={searchText}
-            onSubmit={onSubmit}
-            onResultPress={onAutocompleteResultPress}
-            onProfileClick={handleProfileClick}
-          />
-        ) : (
-          <SearchHistory
-            searchHistory={termHistory}
-            selectedProfiles={accountHistoryProfiles?.profiles || []}
-            onItemClick={handleHistoryItemClick}
-            onProfileClick={handleProfileClick}
-            onRemoveItemClick={deleteSearchHistoryItem}
-            onRemoveProfileClick={deleteProfileHistoryItem}
-          />
-        )}
-      </View>
-      <View
-        style={{
-          display: showAutocomplete ? 'none' : 'flex',
-          flex: 1,
-        }}>
-        <SearchScreenInner
-          query={query}
-          queryWithParams={queryWithParams}
-          headerHeight={headerHeight}
-        />
-      </View>
-    </Layout.Screen>
-  )
-}
-
-let AutocompleteResults = ({
-  isAutocompleteFetching,
-  autocompleteData,
-  searchText,
-  onSubmit,
-  onResultPress,
-  onProfileClick,
-}: {
-  isAutocompleteFetching: boolean
-  autocompleteData: AppBskyActorDefs.ProfileViewBasic[] | undefined
-  searchText: string
-  onSubmit: () => void
-  onResultPress: () => void
-  onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
-}): React.ReactNode => {
-  const moderationOpts = useModerationOpts()
-  const {_} = useLingui()
-  return (
-    <>
-      {(isAutocompleteFetching && !autocompleteData?.length) ||
-      !moderationOpts ? (
-        <Loader />
-      ) : (
-        <Layout.Content
-          keyboardShouldPersistTaps="handled"
-          keyboardDismissMode="on-drag">
-          <SearchLinkCard
-            label={_(msg`Search for "${searchText}"`)}
-            onPress={isNative ? onSubmit : undefined}
-            to={
-              isNative
-                ? undefined
-                : `/search?q=${encodeURIComponent(searchText)}`
-            }
-            style={{borderBottomWidth: 1}}
-          />
-          {autocompleteData?.map(item => (
-            <SearchProfileCard
-              key={item.did}
-              profile={item}
-              moderation={moderateProfile(item, moderationOpts)}
-              onPress={() => {
-                onProfileClick(item)
-                onResultPress()
-              }}
-            />
-          ))}
-          <View style={{height: 200}} />
-        </Layout.Content>
-      )}
-    </>
-  )
-}
-AutocompleteResults = React.memo(AutocompleteResults)
-
-function SearchHistory({
-  searchHistory,
-  selectedProfiles,
-  onItemClick,
-  onProfileClick,
-  onRemoveItemClick,
-  onRemoveProfileClick,
-}: {
-  searchHistory: string[]
-  selectedProfiles: AppBskyActorDefs.ProfileViewDetailed[]
-  onItemClick: (item: string) => void
-  onProfileClick: (profile: AppBskyActorDefs.ProfileViewDetailed) => void
-  onRemoveItemClick: (item: string) => void
-  onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewDetailed) => void
-}) {
-  const {gtMobile} = useBreakpoints()
-  const t = useTheme()
-  const {_} = useLingui()
-
-  return (
-    <Layout.Content
-      keyboardDismissMode="interactive"
-      keyboardShouldPersistTaps="handled">
-      <View style={[a.w_full, a.px_md]}>
-        {(searchHistory.length > 0 || selectedProfiles.length > 0) && (
-          <Text style={[a.text_md, a.font_bold, a.p_md]}>
-            <Trans>Recent Searches</Trans>
-          </Text>
-        )}
-        {selectedProfiles.length > 0 && (
-          <View
-            style={[
-              styles.selectedProfilesContainer,
-              !gtMobile && styles.selectedProfilesContainerMobile,
-            ]}>
-            <RNGHScrollView
-              keyboardShouldPersistTaps="handled"
-              horizontal={true}
-              style={[
-                a.flex_row,
-                a.flex_nowrap,
-                {marginHorizontal: tokens.space._2xl * -1},
-              ]}
-              contentContainerStyle={[a.px_2xl, a.border_0]}>
-              {selectedProfiles.slice(0, 5).map((profile, index) => (
-                <View
-                  key={index}
-                  style={[
-                    styles.profileItem,
-                    !gtMobile && styles.profileItemMobile,
-                  ]}>
-                  <Link
-                    href={makeProfileLink(profile)}
-                    title={profile.handle}
-                    asAnchor
-                    anchorNoUnderline
-                    onBeforePress={() => onProfileClick(profile)}
-                    style={[a.align_center, a.w_full]}>
-                    <UserAvatar
-                      avatar={profile.avatar}
-                      type={profile.associated?.labeler ? 'labeler' : 'user'}
-                      size={60}
-                    />
-                    <Text
-                      emoji
-                      style={[a.text_xs, a.text_center, styles.profileName]}
-                      numberOfLines={1}>
-                      {sanitizeDisplayName(
-                        profile.displayName || profile.handle,
-                      )}
-                    </Text>
-                  </Link>
-                  <Pressable
-                    accessibilityRole="button"
-                    accessibilityLabel={_(msg`Remove profile`)}
-                    accessibilityHint={_(
-                      msg`Removes profile from search history`,
-                    )}
-                    onPress={() => onRemoveProfileClick(profile)}
-                    hitSlop={createHitslop(6)}
-                    style={styles.profileRemoveBtn}>
-                    <XIcon size="xs" style={t.atoms.text_contrast_low} />
-                  </Pressable>
-                </View>
-              ))}
-            </RNGHScrollView>
-          </View>
-        )}
-        {searchHistory.length > 0 && (
-          <View style={[a.pl_md, a.pr_xs, a.mt_md]}>
-            {searchHistory.slice(0, 5).map((historyItem, index) => (
-              <View key={index} style={[a.flex_row, a.align_center, a.mt_xs]}>
-                <Pressable
-                  accessibilityRole="button"
-                  onPress={() => onItemClick(historyItem)}
-                  hitSlop={HITSLOP_10}
-                  style={[a.flex_1, a.py_md]}>
-                  <Text style={[a.text_md]}>{historyItem}</Text>
-                </Pressable>
-                <Button
-                  label={_(msg`Remove ${historyItem}`)}
-                  onPress={() => onRemoveItemClick(historyItem)}
-                  size="small"
-                  variant="ghost"
-                  color="secondary"
-                  shape="round">
-                  <ButtonIcon icon={XIcon} />
-                </Button>
-              </View>
-            ))}
-          </View>
-        )}
-      </View>
-    </Layout.Content>
-  )
-}
-
-function scrollToTopWeb() {
-  if (isWeb) {
-    window.scrollTo(0, 0)
-  }
-}
-
-const styles = StyleSheet.create({
-  selectedProfilesContainer: {
-    marginTop: 10,
-    paddingHorizontal: 12,
-    height: 80,
-  },
-  selectedProfilesContainerMobile: {
-    height: 100,
-  },
-  profileItem: {
-    alignItems: 'center',
-    marginRight: 15,
-    width: 78,
-  },
-  profileItemMobile: {
-    width: 70,
-  },
-  profileName: {
-    width: 78,
-    marginTop: 6,
-  },
-  profileRemoveBtn: {
-    position: 'absolute',
-    top: 0,
-    right: 5,
-    backgroundColor: 'white',
-    borderRadius: 10,
-    width: 18,
-    height: 18,
-    alignItems: 'center',
-    justifyContent: 'center',
-  },
-})
diff --git a/src/view/screens/Search/index.tsx b/src/view/screens/Search/index.tsx
deleted file mode 100644
index f6c0eca26..000000000
--- a/src/view/screens/Search/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export {SearchScreen} from '#/view/screens/Search/Search'
diff --git a/src/view/shell/desktop/SidebarTrendingTopics.tsx b/src/view/shell/desktop/SidebarTrendingTopics.tsx
index a7b9a8391..db9492349 100644
--- a/src/view/shell/desktop/SidebarTrendingTopics.tsx
+++ b/src/view/shell/desktop/SidebarTrendingTopics.tsx
@@ -14,7 +14,7 @@ import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonIcon} from '#/components/Button'
 import {Divider} from '#/components/Divider'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
-import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
+import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
 import * as Prompt from '#/components/Prompt'
 import {
   TrendingTopic,