RoversX/LaunchNext/main 418k tokens More Tools
```
├── .gitattributes (omitted)
├── .github/
   ├── ISSUE_TEMPLATE/
      ├── bug_report.md (100 tokens)
      ├── config.yml
      ├── feature_request.md (100 tokens)
   ├── workflows/
      ├── update-homebrew.yml (400 tokens)
├── .gitignore (100 tokens)
├── Archive/
   ├── AI.zip
   ├── CAGridView_first_working_version.swift.zip
├── Config/
   ├── OpenMultitouchSupport.xcconfig (100 tokens)
├── LICENSE (omitted)
├── LaunchNext.xcodeproj/
   ├── project.pbxproj (3.8k tokens)
   ├── project.xcworkspace/
      ├── contents.xcworkspacedata
      ├── xcshareddata/
         ├── WorkspaceSettings.xcsettings
      ├── xcuserdata/
         ├── gzk.xcuserdatad/
            ├── UserInterfaceState.xcuserstate
         ├── roversx.xcuserdatad/
            ├── WorkspaceSettings.xcsettings (100 tokens)
   ├── xcshareddata/
      ├── xcschemes/
         ├── LaunchNext.xcscheme (600 tokens)
   ├── xcuserdata/
      ├── gzk.xcuserdatad/
         ├── xcdebugger/
            ├── Breakpoints_v2.xcbkptlist (300 tokens)
         ├── xcschemes/
            ├── xcschememanagement.plist (100 tokens)
      ├── roversx.xcuserdatad/
         ├── xcschemes/
            ├── xcschememanagement.plist (100 tokens)
├── LaunchNext/
   ├── Animations.swift (300 tokens)
   ├── AppCacheManager.swift (2.4k tokens)
   ├── AppIcon.icon/
      ├── Assets/
         ├── 2.searchbar_unspecified_unspecified_automatic.svg (100 tokens)
         ├── 3.magnifier_unspecified_unspecified_automatic.svg (200 tokens)
         ├── 4.chiclet6_unspecified_unspecified_automatic.svg (600 tokens)
         ├── 5.chiclet5_unspecified_unspecified_automatic.svg (600 tokens)
         ├── 6.chiclet4_unspecified_unspecified_automatic.svg (600 tokens)
         ├── 7.chiclet3_unspecified_unspecified_automatic.svg (600 tokens)
         ├── 8.chiclet2_unspecified_unspecified_automatic.svg (600 tokens)
         ├── 9.chiclet1_unspecified_unspecified_automatic.svg (600 tokens)
      ├── icon.json (400 tokens)
   ├── AppInfo.swift (1300 tokens)
   ├── AppStore.swift (55.8k tokens)
   ├── Assets.xcassets/
      ├── AboutBackground.imageset/
         ├── Contents.json (100 tokens)
         ├── background_dark.heic
         ├── background_light.heic
      ├── AccentColor.colorset/
         ├── Contents.json
      ├── AppIcon.appiconset/
         ├── Contents.json (200 tokens)
      ├── AppearanceAuto.imageset/
         ├── Contents.json
         ├── auto_appearance_mode.heic
      ├── AppearanceDark.imageset/
         ├── Contents.json
         ├── dark_appearance_mode.heic
      ├── AppearanceLight.imageset/
         ├── Contents.json
         ├── light_appearance_mode.heic
      ├── Contents.json
      ├── FoundaryColor.colorset/
         ├── Contents.json (200 tokens)
   ├── CAFolderGridView.swift (10.6k tokens)
   ├── CAFolderGridViewRepresentable.swift (1000 tokens)
   ├── CAGridView+Input.swift (11.9k tokens)
   ├── CAGridView+Layout.swift (3.6k tokens)
   ├── CAGridView.swift (5.9k tokens)
   ├── CAGridViewRepresentable.swift (3.6k tokens)
   ├── ControllerInputManager.swift (2k tokens)
   ├── Extensions.swift (200 tokens)
   ├── FolderInfo.swift (2.2k tokens)
   ├── FolderView.swift (6.6k tokens)
   ├── GeometryUtils.swift (800 tokens)
   ├── Gesture/
      ├── GestureConfiguration.swift (200 tokens)
      ├── GestureInputDevice.swift (200 tokens)
      ├── GestureMonitor.swift (500 tokens)
      ├── GestureStateMachine.swift (3.3k tokens)
      ├── GestureTouchProvider.swift (900 tokens)
   ├── HotCornerMonitor.swift (900 tokens)
   ├── IconStore.swift (200 tokens)
   ├── LaunchNextCLI.swift (8.6k tokens)
   ├── LaunchNextCLIIPC.swift (2.2k tokens)
   ├── LaunchpadApp.swift (14.3k tokens)
   ├── LaunchpadItemButton.swift (1800 tokens)
   ├── LaunchpadView.swift (29.5k tokens)
   ├── LayoutPresetCatalog.swift (1000 tokens)
   ├── Localization.swift (85.7k tokens)
   ├── Markdown/
      ├── MarkdownRenderModel.swift (200 tokens)
      ├── ReleaseNotesMarkdownView.swift (2.1k tokens)
      ├── SimpleMarkdownParser.swift (1800 tokens)
   ├── NativeLaunchpadImporter.swift (5.6k tokens)
   ├── PerformanceMode.swift (100 tokens)
   ├── RightClickMenu.swift (2.8k tokens)
   ├── Search/
      ├── FuzzyMatcher.swift (400 tokens)
      ├── LaunchpadSearchEngine.swift (700 tokens)
      ├── SearchIndexEntry.swift (200 tokens)
   ├── SettingsView.swift (51.4k tokens)
   ├── SoundManager.swift (800 tokens)
   ├── ThirdParty/
      ├── OpenMultitouchSupport/
         ├── Framework/
            ├── OpenMultitouchSupportXCF/
               ├── OpenMTEvent.h (100 tokens)
               ├── OpenMTEvent.m (200 tokens)
               ├── OpenMTEventInternal.h (100 tokens)
               ├── OpenMTInternal.h (2k tokens)
               ├── OpenMTListener.h (100 tokens)
               ├── OpenMTListener.m (200 tokens)
               ├── OpenMTListenerInternal.h (100 tokens)
               ├── OpenMTManager.h (300 tokens)
               ├── OpenMTManager.m (4.5k tokens)
               ├── OpenMTManagerInternal.h (100 tokens)
               ├── OpenMTTouch.h (200 tokens)
               ├── OpenMTTouch.m (400 tokens)
               ├── OpenMTTouchInternal.h (200 tokens)
               ├── OpenMultitouchSupportXCF.h (100 tokens)
            ├── module.modulemap (100 tokens)
         ├── Sources/
            ├── OpenMultitouchSupport/
               ├── OMSManager.swift (1300 tokens)
               ├── OMSTouchData.swift (400 tokens)
   ├── VoiceManager.swift (900 tokens)
├── README.md (1800 tokens)
├── UpdaterScripts/
   ├── SwiftUpdater/
      ├── .gitignore
      ├── LICENSE (200 tokens)
      ├── Package.swift (100 tokens)
      ├── README.md (600 tokens)
      ├── Sources/
         ├── SwiftUpdater/
            ├── Arguments.swift (400 tokens)
            ├── Config.swift (300 tokens)
            ├── Downloader.swift (400 tokens)
            ├── Errors.swift (100 tokens)
            ├── GitHubAPI.swift (300 tokens)
            ├── Installer.swift (900 tokens)
            ├── Localization.swift (6.2k tokens)
            ├── NCursesUI.swift (1900 tokens)
            ├── SwiftUpdater.swift (3.7k tokens)
   ├── launchnext_updater.py (10.1k tokens)
├── cs.lproj/
   ├── Localizable.strings
├── de.lproj/
   ├── Localizable.strings
├── en.lproj/
   ├── Localizable.strings
├── es.lproj/
   ├── Localizable.strings
├── fr.lproj/
   ├── Localizable.strings
├── hi.lproj/
   ├── Localizable.strings
├── i18n/
   ├── README.cs.md (1700 tokens)
   ├── README.de.md (1800 tokens)
   ├── README.es.md (1800 tokens)
   ├── README.fr.md (1900 tokens)
   ├── README.hi.md (1700 tokens)
   ├── README.it.md (1800 tokens)
   ├── README.ja.md (1100 tokens)
   ├── README.ko.md (1100 tokens)
   ├── README.ru.md (1800 tokens)
   ├── README.vi.md (1700 tokens)
   ├── README.zh-TW.md (1000 tokens)
   ├── README.zh.md (1000 tokens)
├── it.lproj/
   ├── Localizable.strings
├── ja.lproj/
   ├── Localizable.strings
├── ko.lproj/
   ├── Localizable.strings
├── pt-BR.lproj/
   ├── Localizable.strings
├── public/
   ├── banner.webp
   ├── banner_old.webp
   ├── setting1.webp
   ├── setting2.webp
   ├── setting3.webp
├── ru.lproj/
   ├── Localizable.strings
├── scripts/
   ├── release.sh (300 tokens)
├── vi.lproj/
   ├── Localizable.strings
├── zh-Hans.lproj/
   ├── Localizable.strings
├── zh-Hant.lproj/
   ├── Localizable.strings
```


## /.github/ISSUE_TEMPLATE/bug_report.md

---
name: Bug Report
about: Report an issue
title: "[BUG] <title>"
labels: "bug"
---

### Description of the issue

<!-- A clear and concise description of the issue encountered -->
<!-- Please post in English. Requests in other languages will be ignored and closed. -->

### Steps to reproduce this issue

<!-- 
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
.-->

### Expected behavior

<!-- A clear and concise description of what you expected to happen. -->

### Additional content

<!-- If applicable, add screenshots or screen recordings to help explain your problem. -->


## /.github/ISSUE_TEMPLATE/config.yml

```yml path="/.github/ISSUE_TEMPLATE/config.yml" 
blank_issues_enabled: true

```

## /.github/ISSUE_TEMPLATE/feature_request.md

---
name: Feature Request
about: Suggest an idea or improvement
title: "[FR] <title>"
labels: "enhancement"
---

### Feature Request

<!-- A clear and concise description of the problem you are facing. Example: "I’m always frustrated when..." -->
<!-- Please post in English. Requests in other languages will be ignored and closed. -->

### Solution you would like

<!-- A clear and concise description of what you want to happen. -->

### Alternatives you have considered

<!-- A clear and concise description of any alternative solutions or features you've considered. -->

### Additional context

<!-- Add any other context, screenshots, or mockups about the feature request here.-->


## /.github/workflows/update-homebrew.yml

```yml path="/.github/workflows/update-homebrew.yml" 
name: Update Homebrew Tap

on:
  release:
    types:
      - published

permissions:
  contents: read

jobs:
  update-homebrew:
    runs-on: macos-latest
    steps:
      - name: Read checksum from release assets
        id: checksum
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ github.event.release.tag_name }}
          REPO: ${{ github.repository }}
        run: |
          set -euo pipefail

          VERSION="${TAG#v}"
          ZIP_NAME="LaunchNext${VERSION}.zip"

          gh release download "${TAG}" \
            --repo "${REPO}" \
            --pattern "checksums.txt" \
            --dir .

          SHA256="$(awk -v file="${ZIP_NAME}" '$2 == file { print $1 }' checksums.txt)"
          if [[ -z "${SHA256}" ]]; then
            echo "Could not find checksum for ${ZIP_NAME} in checksums.txt" >&2
            exit 1
          fi

          echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
          echo "sha256=${SHA256}" >> "${GITHUB_OUTPUT}"

      - name: Update tap cask
        env:
          TAP_TOKEN: ${{ secrets.TAP_TOKEN }}
          TAG: ${{ github.event.release.tag_name }}
          SHA256: ${{ steps.checksum.outputs.sha256 }}
        run: |
          set -euo pipefail

          if [[ -z "${TAP_TOKEN}" ]]; then
            echo "TAP_TOKEN is not configured; skipping Homebrew tap update."
            exit 0
          fi

          curl -X POST \
            -H "Authorization: token ${TAP_TOKEN}" \
            -H "Accept: application/vnd.github.v3+json" \
            https://api.github.com/repos/RoversX/homebrew-tap/actions/workflows/update-formula.yml/dispatches \
            -d "{\"ref\":\"main\",\"inputs\":{\"formula\":\"launchnext\",\"tag\":\"${TAG}\",\"repository\":\"RoversX/LaunchNext\",\"sha256\":\"${SHA256}\"}}"

```

## /.gitignore

```gitignore path="/.gitignore" 
# Xcode
*.xcuserstate
xcuserdata/
*.xcuserdatad

# Build artifacts
build/
DerivedData/
.build/
dist/

# macOS
.DS_Store
.AppleDouble
.LSOverride

# Swift updater build artifacts
UpdaterScripts/SwiftUpdater/.build/
UpdaterScripts/SwiftUpdater/.build-*/

# Archives
*.xcarchive

# Breakpoints
*.xcbkptlist

# Parallel sources
parallel_version/

# Local AI notes
LaunchNext/AI/TODO.md

# Local docs draft
/docs/

```

## /Archive/AI.zip

Binary file available at https://raw.githubusercontent.com/RoversX/LaunchNext/refs/heads/main/Archive/AI.zip

## /Archive/CAGridView_first_working_version.swift.zip

Binary file available at https://raw.githubusercontent.com/RoversX/LaunchNext/refs/heads/main/Archive/CAGridView_first_working_version.swift.zip

## /Config/OpenMultitouchSupport.xcconfig

```xcconfig path="/Config/OpenMultitouchSupport.xcconfig" 
// Experimental gesture support wiring.
// If this feature is removed later, delete this file and remove the
// OpenMultitouchSupport.xcconfig baseConfigurationReference lines from
// LaunchNext.xcodeproj/project.pbxproj.
FRAMEWORK_SEARCH_PATHS = $(inherited) /System/Library/PrivateFrameworks
HEADER_SEARCH_PATHS = $(inherited) $(SRCROOT)/LaunchNext/ThirdParty/OpenMultitouchSupport/Framework
OTHER_LDFLAGS = $(inherited) -framework MultitouchSupport
SWIFT_INCLUDE_PATHS = $(inherited) $(SRCROOT)/LaunchNext/ThirdParty/OpenMultitouchSupport/Framework

```

## /LaunchNext.xcodeproj/project.pbxproj

```pbxproj path="/LaunchNext.xcodeproj/project.pbxproj" 
// !$*UTF8*$!
{
	archiveVersion = 1;
	classes = {
	};
	objectVersion = 77;
	objects = {

/* Begin PBXBuildFile section */
		A1E2B5C02F4A8A1B00F12345 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A1E2B5CB2F4A8A1B00F12345 /* Localizable.strings */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
		0D1B0B402E4CDE460083EEF9 /* LaunchNext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LaunchNext.app; sourceTree = BUILT_PRODUCTS_DIR; };
		0F7A6B1C3E2D4F5A6B7C8D9E /* OpenMultitouchSupport.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = OpenMultitouchSupport.xcconfig; sourceTree = "<group>"; };
		690FA1FB2E88822200948FA6 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
		690FA1FC2E88822200948FA6 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
		A1E2B5C12F4A8A1B00F12345 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
		A1E2B5C22F4A8A1B00F12345 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
		A1E2B5CD2F4A8A1B00F12345 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
		A1E2B5C32F4A8A1B00F12345 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
		A1E2B5C42F4A8A1B00F12345 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
		A1E2B5C52F4A8A1B00F12345 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
		A1E2B5C62F4A8A1B00F12345 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
		A1E2B5C72F4A8A1B00F12345 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
		A1E2B5C82F4A8A1B00F12345 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
		A1E2B5C92F4A8A1B00F12345 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = "<group>"; };
		A1E2B5CA2F4A8A1B00F12345 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
		A1E2B5CC2F4A8A1B00F12345 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
		0D1B0B422E4CDE460083EEF9 /* LaunchNext */ = {
			isa = PBXFileSystemSynchronizedRootGroup;
			path = LaunchNext;
			sourceTree = "<group>";
		};
/* End PBXFileSystemSynchronizedRootGroup section */

/* Begin PBXFrameworksBuildPhase section */
		0D1B0B3D2E4CDE460083EEF9 /* Frameworks */ = {
			isa = PBXFrameworksBuildPhase;
			buildActionMask = 2147483647;
			files = (
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
		0D1B0B372E4CDE460083EEF9 = {
			isa = PBXGroup;
			children = (
				A1E2B5CB2F4A8A1B00F12345 /* Localizable.strings */,
				0F7A6B1D3E2D4F5A6B7C8D9E /* Config */,
				0D1B0B422E4CDE460083EEF9 /* LaunchNext */,
				0D1B0B412E4CDE460083EEF9 /* Products */,
			);
			sourceTree = "<group>";
		};
		0D1B0B412E4CDE460083EEF9 /* Products */ = {
			isa = PBXGroup;
			children = (
				0D1B0B402E4CDE460083EEF9 /* LaunchNext.app */,
			);
			name = Products;
			sourceTree = "<group>";
		};
		0F7A6B1D3E2D4F5A6B7C8D9E /* Config */ = {
			isa = PBXGroup;
			children = (
				0F7A6B1C3E2D4F5A6B7C8D9E /* OpenMultitouchSupport.xcconfig */,
			);
			path = Config;
			sourceTree = "<group>";
		};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
		0D1B0B3F2E4CDE460083EEF9 /* LaunchNext */ = {
			isa = PBXNativeTarget;
			buildConfigurationList = 0D1B0B4B2E4CDE480083EEF9 /* Build configuration list for PBXNativeTarget "LaunchNext" */;
			buildPhases = (
				0D1B0B3C2E4CDE460083EEF9 /* Sources */,
				0D1B0B3D2E4CDE460083EEF9 /* Frameworks */,
				0D1B0B3E2E4CDE460083EEF9 /* Resources */,
				E1C2D3A42F56B78900ABCDEF /* Run Script (Updater) */,
			);
			buildRules = (
			);
			dependencies = (
			);
			fileSystemSynchronizedGroups = (
				0D1B0B422E4CDE460083EEF9 /* LaunchNext */,
			);
			name = LaunchNext;
			packageProductDependencies = (
			);
			productName = LaunchNext;
			productReference = 0D1B0B402E4CDE460083EEF9 /* LaunchNext.app */;
			productType = "com.apple.product-type.application";
		};
/* End PBXNativeTarget section */

/* Begin PBXProject section */
		0D1B0B382E4CDE460083EEF9 /* Project object */ = {
			isa = PBXProject;
			attributes = {
				BuildIndependentTargetsInParallel = 1;
				LastSwiftUpdateCheck = 2600;
				LastUpgradeCheck = 2620;
				TargetAttributes = {
					0D1B0B3F2E4CDE460083EEF9 = {
						CreatedOnToolsVersion = 26.0;
						LastSwiftMigration = 2600;
					};
				};
			};
			buildConfigurationList = 0D1B0B3B2E4CDE460083EEF9 /* Build configuration list for PBXProject "LaunchNext" */;
			developmentRegion = en;
			hasScannedForEncodings = 0;
			knownRegions = (
				en,
				Base,
				"zh-Hans",
				"zh-Hant",
				ja,
				ko,
				fr,
				es,
				cs,
				de,
				ru,
				hi,
				vi,
				it,
				"pt-BR",
			);
			mainGroup = 0D1B0B372E4CDE460083EEF9;
			minimizedProjectReferenceProxies = 1;
			preferredProjectObjectVersion = 77;
			productRefGroup = 0D1B0B412E4CDE460083EEF9 /* Products */;
			projectDirPath = "";
			projectRoot = "";
			targets = (
				0D1B0B3F2E4CDE460083EEF9 /* LaunchNext */,
			);
		};
/* End PBXProject section */

/* Begin PBXResourcesBuildPhase section */
		0D1B0B3E2E4CDE460083EEF9 /* Resources */ = {
			isa = PBXResourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				A1E2B5C02F4A8A1B00F12345 /* Localizable.strings in Resources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
		E1C2D3A42F56B78900ABCDEF /* Run Script (Updater) */ = {
			isa = PBXShellScriptBuildPhase;
			buildActionMask = 2147483647;
			files = (
			);
			inputFileListPaths = (
			);
			inputPaths = (
				"$(SRCROOT)/UpdaterScripts/SwiftUpdater/Package.swift",
				"$(SRCROOT)/UpdaterScripts/SwiftUpdater/Sources/SwiftUpdater/Arguments.swift",
				"$(SRCROOT)/UpdaterScripts/SwiftUpdater/Sources/SwiftUpdater/Config.swift",
				"$(SRCROOT)/UpdaterScripts/SwiftUpdater/Sources/SwiftUpdater/Downloader.swift",
				"$(SRCROOT)/UpdaterScripts/SwiftUpdater/Sources/SwiftUpdater/Errors.swift",
				"$(SRCROOT)/UpdaterScripts/SwiftUpdater/Sources/SwiftUpdater/GitHubAPI.swift",
				"$(SRCROOT)/UpdaterScripts/SwiftUpdater/Sources/SwiftUpdater/Installer.swift",
				"$(SRCROOT)/UpdaterScripts/SwiftUpdater/Sources/SwiftUpdater/Localization.swift",
				"$(SRCROOT)/UpdaterScripts/SwiftUpdater/Sources/SwiftUpdater/NCursesUI.swift",
				"$(SRCROOT)/UpdaterScripts/SwiftUpdater/Sources/SwiftUpdater/SwiftUpdater.swift",
			);
			name = "Run Script (Updater)";
			outputFileListPaths = (
			);
			outputPaths = (
				"$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/Updater/SwiftUpdater",
			);
			runOnlyForDeploymentPostprocessing = 0;
			shellPath = /bin/bash;
			shellScript = "set -euo pipefail\n\nif [[ \"${ENABLE_PREVIEWS:-NO}\" == \"YES\" ]]; then\n  echo 'SwiftUI Preview: skip updater build'\n  exit 0\nfi\n\nUPDATER_DIR=\"${SRCROOT}/UpdaterScripts/SwiftUpdater\"\nDEST_DIR=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Updater\"\nSOURCE_BIN=\"${UPDATER_DIR}/.build/apple/Products/Release/SwiftUpdater\"\nLEGACY_BIN=\"${UPDATER_DIR}/.build/release/SwiftUpdater\"\n\nmkdir -p \"${DEST_DIR}\"\n\nfunction sign_binary {\n  /usr/bin/codesign --force --sign - --preserve-metadata=entitlements,requirements \"$1\"\n}\n\nif [[ -f \"${SOURCE_BIN}\" ]]; then\n  echo \"Using SwiftUpdater at ${SOURCE_BIN}\"\n  cp \"${SOURCE_BIN}\" \"${DEST_DIR}/SwiftUpdater\"\n  chmod +x \"${DEST_DIR}/SwiftUpdater\"\n  sign_binary \"${DEST_DIR}/SwiftUpdater\"\n  exit 0\nfi\n\nif [[ -f \"${LEGACY_BIN}\" ]]; then\n  echo \"Using SwiftUpdater at ${LEGACY_BIN}\"\n  cp \"${LEGACY_BIN}\" \"${DEST_DIR}/SwiftUpdater\"\n  chmod +x \"${DEST_DIR}/SwiftUpdater\"\n  sign_binary \"${DEST_DIR}/SwiftUpdater\"\n  exit 0\nfi\n\n>&2 echo \"error: SwiftUpdater binary not found. Run 'swift build --configuration release --arch arm64 --arch x86_64 --product SwiftUpdater' in UpdaterScripts/SwiftUpdater\"\nexit 1\n";
		};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
		0D1B0B3C2E4CDE460083EEF9 /* Sources */ = {
			isa = PBXSourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXSourcesBuildPhase section */

/* Begin PBXVariantGroup section */
		A1E2B5CB2F4A8A1B00F12345 /* Localizable.strings */ = {
			isa = PBXVariantGroup;
			children = (
				A1E2B5C12F4A8A1B00F12345 /* en */,
				A1E2B5C22F4A8A1B00F12345 /* zh-Hans */,
				A1E2B5CD2F4A8A1B00F12345 /* zh-Hant */,
				A1E2B5C32F4A8A1B00F12345 /* ja */,
				A1E2B5C42F4A8A1B00F12345 /* ko */,
				A1E2B5C52F4A8A1B00F12345 /* fr */,
				A1E2B5C62F4A8A1B00F12345 /* es */,
				690FA1FC2E88822200948FA6 /* it */,
				690FA1FB2E88822200948FA6 /* cs */,
				A1E2B5C72F4A8A1B00F12345 /* de */,
				A1E2B5C82F4A8A1B00F12345 /* ru */,
				A1E2B5C92F4A8A1B00F12345 /* hi */,
				A1E2B5CA2F4A8A1B00F12345 /* vi */,
				A1E2B5CC2F4A8A1B00F12345 /* pt-BR */,
			);
			name = Localizable.strings;
			sourceTree = "<group>";
		};
/* End PBXVariantGroup section */

/* Begin XCBuildConfiguration section */
		0D1B0B492E4CDE480083EEF9 /* Debug */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_ENABLE_OBJC_WEAK = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				COPY_PHASE_STRIP = NO;
				DEAD_CODE_STRIPPING = YES;
				DEBUG_INFORMATION_FORMAT = dwarf;
				DEVELOPMENT_TEAM = 6V3LHNB5K8;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				ENABLE_TESTABILITY = YES;
				ENABLE_USER_SCRIPT_SANDBOXING = NO;
				GCC_C_LANGUAGE_STANDARD = gnu17;
				GCC_DYNAMIC_NO_PIC = NO;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_OPTIMIZATION_LEVEL = 0;
				GCC_PREPROCESSOR_DEFINITIONS = (
					"DEBUG=1",
					"$(inherited)",
				);
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
				MACOSX_DEPLOYMENT_TARGET = 26.0;
				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
				MTL_FAST_MATH = YES;
				ONLY_ACTIVE_ARCH = YES;
				SDKROOT = macosx;
				STRING_CATALOG_GENERATE_SYMBOLS = YES;
				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
			};
			name = Debug;
		};
		0D1B0B4A2E4CDE480083EEF9 /* Release */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_ENABLE_OBJC_WEAK = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				COPY_PHASE_STRIP = NO;
				DEAD_CODE_STRIPPING = YES;
				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
				DEVELOPMENT_TEAM = 6V3LHNB5K8;
				ENABLE_NS_ASSERTIONS = NO;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				ENABLE_USER_SCRIPT_SANDBOXING = NO;
				GCC_C_LANGUAGE_STANDARD = gnu17;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
				MACOSX_DEPLOYMENT_TARGET = 26.0;
				MTL_ENABLE_DEBUG_INFO = NO;
				MTL_FAST_MATH = YES;
				SDKROOT = macosx;
				STRING_CATALOG_GENERATE_SYMBOLS = YES;
				SWIFT_COMPILATION_MODE = wholemodule;
			};
			name = Release;
		};
		0D1B0B4C2E4CDE480083EEF9 /* Debug */ = {
			isa = XCBuildConfiguration;
			baseConfigurationReference = 0F7A6B1C3E2D4F5A6B7C8D9E /* OpenMultitouchSupport.xcconfig */;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
				CLANG_ENABLE_MODULES = YES;
				CODE_SIGN_STYLE = Automatic;
				COMBINE_HIDPI_IMAGES = YES;
				CURRENT_PROJECT_VERSION = 20260224;
				DEAD_CODE_STRIPPING = YES;
				DEVELOPMENT_TEAM = "";
				ENABLE_APP_SANDBOX = NO;
				ENABLE_HARDENED_RUNTIME = YES;
				ENABLE_PREVIEWS = YES;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_KEY_CFBundleAllowMixedLocalizations = YES;
				INFOPLIST_KEY_CFBundleDisplayName = LaunchNext;
				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
				INFOPLIST_KEY_LSUIElement = YES;
				INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 RoversX / CloseX. Licensed under GPL-3.0";
				LD_RUNPATH_SEARCH_PATHS = (
					"$(inherited)",
					"@executable_path/../Frameworks",
				);
				MARKETING_VERSION = 2.4.1;
				PRODUCT_BUNDLE_IDENTIFIER = LaunchNext;
				PRODUCT_NAME = "$(TARGET_NAME)";
				REGISTER_APP_GROUPS = YES;
				STRING_CATALOG_GENERATE_SYMBOLS = YES;
				SWIFT_APPROACHABLE_CONCURRENCY = YES;
				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
				SWIFT_EMIT_LOC_STRINGS = YES;
				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
				SWIFT_VERSION = 5.0;
			};
			name = Debug;
		};
		0D1B0B4D2E4CDE480083EEF9 /* Release */ = {
			isa = XCBuildConfiguration;
			baseConfigurationReference = 0F7A6B1C3E2D4F5A6B7C8D9E /* OpenMultitouchSupport.xcconfig */;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
				CLANG_ENABLE_MODULES = YES;
				CODE_SIGN_STYLE = Automatic;
				COMBINE_HIDPI_IMAGES = YES;
				CURRENT_PROJECT_VERSION = 20260224;
				DEAD_CODE_STRIPPING = YES;
				DEVELOPMENT_TEAM = "";
				ENABLE_APP_SANDBOX = NO;
				ENABLE_HARDENED_RUNTIME = YES;
				ENABLE_PREVIEWS = YES;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_KEY_CFBundleAllowMixedLocalizations = YES;
				INFOPLIST_KEY_CFBundleDisplayName = LaunchNext;
				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
				INFOPLIST_KEY_LSUIElement = YES;
				INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 RoversX / CloseX. Licensed under GPL-3.0";
				LD_RUNPATH_SEARCH_PATHS = (
					"$(inherited)",
					"@executable_path/../Frameworks",
				);
				MARKETING_VERSION = 2.4.1;
				PRODUCT_BUNDLE_IDENTIFIER = LaunchNext;
				PRODUCT_NAME = "$(TARGET_NAME)";
				REGISTER_APP_GROUPS = YES;
				STRING_CATALOG_GENERATE_SYMBOLS = YES;
				SWIFT_APPROACHABLE_CONCURRENCY = YES;
				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
				SWIFT_EMIT_LOC_STRINGS = YES;
				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
				SWIFT_VERSION = 5.0;
			};
			name = Release;
		};
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
		0D1B0B3B2E4CDE460083EEF9 /* Build configuration list for PBXProject "LaunchNext" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				0D1B0B492E4CDE480083EEF9 /* Debug */,
				0D1B0B4A2E4CDE480083EEF9 /* Release */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
		0D1B0B4B2E4CDE480083EEF9 /* Build configuration list for PBXNativeTarget "LaunchNext" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				0D1B0B4C2E4CDE480083EEF9 /* Debug */,
				0D1B0B4D2E4CDE480083EEF9 /* Release */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
/* End XCConfigurationList section */
	};
	rootObject = 0D1B0B382E4CDE460083EEF9 /* Project object */;
}

```

## /LaunchNext.xcodeproj/project.xcworkspace/contents.xcworkspacedata

```xcworkspacedata path="/LaunchNext.xcodeproj/project.xcworkspace/contents.xcworkspacedata" 
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "self:">
   </FileRef>
</Workspace>

```

## /LaunchNext.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

```xcsettings path="/LaunchNext.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings" 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

```

## /LaunchNext.xcodeproj/project.xcworkspace/xcuserdata/gzk.xcuserdatad/UserInterfaceState.xcuserstate

Binary file available at https://raw.githubusercontent.com/RoversX/LaunchNext/refs/heads/main/LaunchNext.xcodeproj/project.xcworkspace/xcuserdata/gzk.xcuserdatad/UserInterfaceState.xcuserstate

## /LaunchNext.xcodeproj/project.xcworkspace/xcuserdata/roversx.xcuserdatad/WorkspaceSettings.xcsettings

```xcsettings path="/LaunchNext.xcodeproj/project.xcworkspace/xcuserdata/roversx.xcuserdatad/WorkspaceSettings.xcsettings" 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>BuildLocationStyle</key>
	<string>UseAppPreferences</string>
	<key>CompilationCachingSetting</key>
	<string>Default</string>
	<key>CustomBuildLocationType</key>
	<string>RelativeToDerivedData</string>
	<key>DerivedDataLocationStyle</key>
	<string>Default</string>
	<key>ShowSharedSchemesAutomaticallyEnabled</key>
	<true/>
</dict>
</plist>

```

## /LaunchNext.xcodeproj/xcshareddata/xcschemes/LaunchNext.xcscheme

```xcscheme path="/LaunchNext.xcodeproj/xcshareddata/xcschemes/LaunchNext.xcscheme" 
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
   LastUpgradeVersion = "2620"
   version = "1.7">
   <BuildAction
      parallelizeBuildables = "YES"
      buildImplicitDependencies = "YES"
      buildArchitectures = "Automatic">
      <BuildActionEntries>
         <BuildActionEntry
            buildForTesting = "YES"
            buildForRunning = "YES"
            buildForProfiling = "YES"
            buildForArchiving = "YES"
            buildForAnalyzing = "YES">
            <BuildableReference
               BuildableIdentifier = "primary"
               BlueprintIdentifier = "0D1B0B3F2E4CDE460083EEF9"
               BuildableName = "LaunchNext.app"
               BlueprintName = "LaunchNext"
               ReferencedContainer = "container:LaunchNext.xcodeproj">
            </BuildableReference>
         </BuildActionEntry>
      </BuildActionEntries>
   </BuildAction>
   <TestAction
      buildConfiguration = "Debug"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      shouldUseLaunchSchemeArgsEnv = "YES"
      shouldAutocreateTestPlan = "YES">
   </TestAction>
   <LaunchAction
      buildConfiguration = "Debug"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      launchStyle = "0"
      useCustomWorkingDirectory = "NO"
      ignoresPersistentStateOnLaunch = "NO"
      debugDocumentVersioning = "YES"
      debugServiceExtension = "internal"
      allowLocationSimulation = "YES">
      <BuildableProductRunnable
         runnableDebuggingMode = "0">
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "0D1B0B3F2E4CDE460083EEF9"
            BuildableName = "LaunchNext.app"
            BlueprintName = "LaunchNext"
            ReferencedContainer = "container:LaunchNext.xcodeproj">
         </BuildableReference>
      </BuildableProductRunnable>
   </LaunchAction>
   <ProfileAction
      buildConfiguration = "Release"
      shouldUseLaunchSchemeArgsEnv = "YES"
      savedToolIdentifier = ""
      useCustomWorkingDirectory = "NO"
      debugDocumentVersioning = "YES">
      <BuildableProductRunnable
         runnableDebuggingMode = "0">
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "0D1B0B3F2E4CDE460083EEF9"
            BuildableName = "LaunchNext.app"
            BlueprintName = "LaunchNext"
            ReferencedContainer = "container:LaunchNext.xcodeproj">
         </BuildableReference>
      </BuildableProductRunnable>
   </ProfileAction>
   <AnalyzeAction
      buildConfiguration = "Debug">
   </AnalyzeAction>
   <ArchiveAction
      buildConfiguration = "Release"
      revealArchiveInOrganizer = "YES">
   </ArchiveAction>
</Scheme>

```

## /LaunchNext.xcodeproj/xcuserdata/gzk.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist

```xcbkptlist path="/LaunchNext.xcodeproj/xcuserdata/gzk.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist" 
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
   uuid = "DE3DF139-2D80-49D6-B259-5D1E522B934E"
   type = "1"
   version = "2.0">
   <Breakpoints>
      <BreakpointProxy
         BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
         <BreakpointContent
            uuid = "8B3FD3CF-DBF9-428D-BA1E-E6D6A4D7E152"
            shouldBeEnabled = "No"
            ignoreCount = "0"
            continueAfterRunningActions = "No"
            filePath = "LaunchNext/LaunchpadView.swift"
            startingColumnNumber = "9223372036854775807"
            endingColumnNumber = "9223372036854775807"
            startingLineNumber = "175"
            endingLineNumber = "175"
            landmarkName = "body"
            landmarkType = "24">
         </BreakpointContent>
      </BreakpointProxy>
      <BreakpointProxy
         BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
         <BreakpointContent
            uuid = "A58C9A88-D4D8-4AD1-8D08-4E69E45161C8"
            shouldBeEnabled = "No"
            ignoreCount = "0"
            continueAfterRunningActions = "No"
            filePath = "LaunchNext/AppStore.swift"
            startingColumnNumber = "9223372036854775807"
            endingColumnNumber = "9223372036854775807"
            startingLineNumber = "867"
            endingLineNumber = "867"
            landmarkName = "isInsideAnotherApp(_:)"
            landmarkType = "7">
         </BreakpointContent>
      </BreakpointProxy>
   </Breakpoints>
</Bucket>

```

## /LaunchNext.xcodeproj/xcuserdata/gzk.xcuserdatad/xcschemes/xcschememanagement.plist

```plist path="/LaunchNext.xcodeproj/xcuserdata/gzk.xcuserdatad/xcschemes/xcschememanagement.plist" 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>SchemeUserState</key>
	<dict>
		<key>LaunchNext.xcscheme_^#shared#^_</key>
		<dict>
			<key>orderHint</key>
			<integer>0</integer>
		</dict>
	</dict>
</dict>
</plist>

```

## /LaunchNext.xcodeproj/xcuserdata/roversx.xcuserdatad/xcschemes/xcschememanagement.plist

```plist path="/LaunchNext.xcodeproj/xcuserdata/roversx.xcuserdatad/xcschemes/xcschememanagement.plist" 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>SchemeUserState</key>
	<dict>
		<key>LaunchNext.xcscheme_^#shared#^_</key>
		<dict>
			<key>orderHint</key>
			<integer>0</integer>
		</dict>
	</dict>
	<key>SuppressBuildableAutocreation</key>
	<dict>
		<key>0D1B0B3F2E4CDE460083EEF9</key>
		<dict>
			<key>primary</key>
			<true/>
		</dict>
	</dict>
</dict>
</plist>

```

## /LaunchNext/Animations.swift

```swift path="/LaunchNext/Animations.swift" 
import SwiftUI

enum LNAnimations {
    // MARK: - Springs - 优化性能的动画配置
    static var springFast: Animation {
        guard AnimationPreferences.isEnabled else { return .linear(duration: 0.0001) }
        return .spring(response: AnimationPreferences.springResponse, dampingFraction: 0.8)
    }
    
    // MARK: - 性能优化的动画
    static var dragPreview: Animation {
        guard AnimationPreferences.isEnabled else { return .linear(duration: 0.0001) }
        return .easeOut(duration: AnimationPreferences.baseDuration)
    }
    static var gridUpdate: Animation {
        guard AnimationPreferences.isEnabled else { return .linear(duration: 0.0001) }
        return .easeInOut(duration: AnimationPreferences.baseDuration)
    }
    
    // MARK: - Transitions
    static var folderOpenTransition: AnyTransition {
        if AnimationPreferences.isEnabled {
            return AnyTransition.scale(scale: 0.95).combined(with: .opacity)
        } else {
            return AnyTransition.opacity
        }
    }
}

private enum AnimationPreferences {
    static var isEnabled: Bool {
        UserDefaults.standard.object(forKey: "enableAnimations") as? Bool ?? true
    }

    static var baseDuration: Double {
        let stored = UserDefaults.standard.double(forKey: "animationDuration")
        let value = stored == 0 ? 0.3 : stored
        return max(0.05, min(value, 1.5))
    }

    static var springResponse: Double {
        max(0.15, baseDuration)
    }
}

```

## /LaunchNext/AppCacheManager.swift

```swift path="/LaunchNext/AppCacheManager.swift" 
import Foundation
import AppKit
import Combine

/// 应用缓存管理器 - 负责缓存应用图标、应用信息和网格布局数据以提高性能
final class AppCacheManager: ObservableObject {
    static let shared = AppCacheManager()
    
    // MARK: - 缓存存储
    private var iconCache: [String: NSImage] = [:]
    private var appInfoCache: [String: AppInfo] = [:]
    private var gridLayoutCache: [String: Any] = [:]
    private let cacheLock = NSLock()
    
    // MARK: - 缓存配置
    private let maxIconCacheSize = 200
    private let maxAppInfoCacheSize = 300
    private var iconCacheOrder: [String] = [] // 改为可变数组,实现真正的LRU
    
    // MARK: - 缓存状态
    @Published var isCacheValid = false
    @Published var lastCacheUpdate = Date.distantPast
    @Published var cacheSize: Int = 0
    // MARK: - 缓存键生成
    private let cacheKeyGenerator = CacheKeyGenerator()

    private init() {}

    private var isLeanMode: Bool {
        PerformanceMode.current == .lean
    }
    // MARK: - 公共接口
    
    /// 生成应用缓存 - 在应用启动或扫描后调用
    func generateCache(from apps: [AppInfo],
                       items: [LaunchpadItem],
                       itemsPerPage: Int,
                       columns: Int,
                       rows: Int) {
        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
            guard let self = self else { return }
            
            // 清空旧缓存
            self.clearAllCaches()
            
            // 收集所有需要缓存的应用,包括文件夹内的应用
            var allApps: [AppInfo] = []
            allApps.append(contentsOf: apps)
            
            // 从items中提取文件夹内的应用
            for item in items {
                if case let .folder(folder) = item {
                    allApps.append(contentsOf: folder.apps)
                }
            }
            
            // 去重,避免重复缓存同一个应用
            var uniqueApps: [AppInfo] = []
            var seenPaths = Set<String>()
            for app in allApps {
                if !seenPaths.contains(app.url.path) {
                    seenPaths.insert(app.url.path)
                    uniqueApps.append(app)
                }
            }
            
            // 缓存应用信息
            self.cacheAppInfos(uniqueApps)
            
            // 缓存应用图标
            if !self.isLeanMode {
                self.cacheAppIcons(uniqueApps)
            }
            
            // 缓存网格布局数据
            self.cacheGridLayout(items,
                                 itemsPerPage: itemsPerPage,
                                 columns: columns,
                                 rows: rows)
            
            DispatchQueue.main.async {
                self.isCacheValid = true
                self.lastCacheUpdate = Date()
                self.calculateCacheSize()
        
            }
        }
    }
    
    /// 获取缓存的应用图标
    func getCachedIcon(for appPath: String) -> NSImage? {
        if isLeanMode {
            return nil
        }
        let key = cacheKeyGenerator.generateIconKey(for: appPath)
        
        cacheLock.lock()
        defer { cacheLock.unlock() }
        if let icon = iconCache[key] {
            if let index = iconCacheOrder.firstIndex(of: key) {
                iconCacheOrder.remove(at: index)
                iconCacheOrder.append(key)
            }
            return icon
        } else {
            return nil
        }
    }
    
    /// 获取缓存的应用信息
    func getCachedAppInfo(for appPath: String) -> AppInfo? {
        let key = cacheKeyGenerator.generateAppInfoKey(for: appPath)
        return appInfoCache[key]
    }
    
    /// 获取缓存的网格布局数据
    func getCachedGridLayout(for layoutKey: String) -> Any? {
        let key = cacheKeyGenerator.generateGridLayoutKey(for: layoutKey)
        return gridLayoutCache[key]
    }
    
    /// 预加载应用图标到缓存
    func preloadIcons(for appPaths: [String]) {
        if isLeanMode {
            return
        }
        DispatchQueue.global(qos: .utility).async { [weak self] in
            guard let self = self else { return }
            
            for path in appPaths {
                if self.getCachedIcon(for: path) == nil {
                    let icon = NSWorkspace.shared.icon(forFile: path)
                    let key = self.cacheKeyGenerator.generateIconKey(for: path)
                    self.cacheLock.lock()
                    self.iconCache[key] = icon
                    self.iconCacheOrder.append(key)
                    if self.iconCache.count > self.maxIconCacheSize {
                        if let oldestKey = self.iconCacheOrder.first {
                            self.iconCache.removeValue(forKey: oldestKey)
                            self.iconCacheOrder.removeFirst()
                        }
                    }
                    self.cacheLock.unlock()
                }
            }
            
            DispatchQueue.main.async {
                self.calculateCacheSize()
            }
        }
    }
    
    /// 智能预加载:预加载当前页面和相邻页面的图标
    func smartPreloadIcons(for items: [LaunchpadItem], currentPage: Int, itemsPerPage: Int) {
        if isLeanMode {
            return
        }
        let startIndex = max(0, (currentPage - 1) * itemsPerPage)
        let endIndex = min(items.count, (currentPage + 2) * itemsPerPage)
        
        let relevantItems = Array(items[startIndex..<endIndex])
        let appPaths = relevantItems.compactMap { item -> String? in
            if case let .app(app) = item {
                return app.url.path
            }
            return nil
        }
        
        preloadIcons(for: appPaths)
    }
    
    /// 清除所有缓存
    func clearAllCaches() {
        cacheLock.lock()
        iconCache.removeAll()
        appInfoCache.removeAll()
        gridLayoutCache.removeAll()
        iconCacheOrder.removeAll()
        cacheLock.unlock()
        
        DispatchQueue.main.async {
            self.isCacheValid = false
            self.cacheSize = 0
        }
    }
    
    /// 清除过期缓存
    func clearExpiredCache() {
        let now = Date()
        let cacheAgeThreshold: TimeInterval = 24 * 60 * 60 // 24小时
        
        if now.timeIntervalSince(lastCacheUpdate) > cacheAgeThreshold {
            clearAllCaches()
        }
    }
    
    /// 手动刷新缓存
    func refreshCache(from apps: [AppInfo],
                      items: [LaunchpadItem],
                      itemsPerPage: Int,
                      columns: Int,
                      rows: Int) {
        // 收集所有需要缓存的应用,包括文件夹内的应用
        var allApps: [AppInfo] = []
        allApps.append(contentsOf: apps)
        
        // 从items中提取文件夹内的应用
        for item in items {
            if case let .folder(folder) = item {
                allApps.append(contentsOf: folder.apps)
            }
        }
        
        // 去重,避免重复缓存同一个应用
        var uniqueApps: [AppInfo] = []
        var seenPaths = Set<String>()
        for app in allApps {
            if !seenPaths.contains(app.url.path) {
                seenPaths.insert(app.url.path)
                uniqueApps.append(app)
            }
        }
        
        generateCache(from: uniqueApps,
                      items: items,
                      itemsPerPage: itemsPerPage,
                      columns: columns,
                      rows: rows)
    }
    
    // MARK: - 私有方法
    
    private func cacheAppInfos(_ apps: [AppInfo]) {
        cacheLock.lock()
        for app in apps {
            let key = cacheKeyGenerator.generateAppInfoKey(for: app.url.path)
            appInfoCache[key] = app
        }
        cacheLock.unlock()
    }
    
    private func cacheAppIcons(_ apps: [AppInfo]) {
        if isLeanMode {
            return
        }
        cacheLock.lock()
        for app in apps {
            let key = cacheKeyGenerator.generateIconKey(for: app.url.path)
            if let existingIndex = iconCacheOrder.firstIndex(of: key) {
                iconCacheOrder.remove(at: existingIndex)
            }
            iconCache[key] = app.icon
            iconCacheOrder.append(key)
            if iconCache.count > maxIconCacheSize {
                if let oldestKey = iconCacheOrder.first {
                    iconCache.removeValue(forKey: oldestKey)
                    iconCacheOrder.removeFirst()
                }
            }
        }
        cacheLock.unlock()
    }
    
    private func cacheGridLayout(_ items: [LaunchpadItem],
                                 itemsPerPage: Int,
                                 columns: Int,
                                 rows: Int) {
        // 缓存网格布局相关的计算数据
        let layoutData = GridLayoutCacheData(
            totalItems: items.count,
            itemsPerPage: itemsPerPage,
            columns: columns,
            rows: rows,
            pageCount: (items.count + max(itemsPerPage, 1) - 1) / max(itemsPerPage, 1)
        )
        let pageInfo = calculatePageInfo(for: items, itemsPerPage: itemsPerPage)
        let key = cacheKeyGenerator.generateGridLayoutKey(for: "main")
        let pageKey = cacheKeyGenerator.generateGridLayoutKey(for: "pages")
        cacheLock.lock()
        gridLayoutCache[key] = layoutData
        gridLayoutCache[pageKey] = pageInfo
        cacheLock.unlock()
        
    }
    
    /// 计算页面信息
    private func calculatePageInfo(for items: [LaunchpadItem], itemsPerPage: Int) -> [PageInfo] {
        let sanitizedItemsPerPage = max(itemsPerPage, 1)
        let pageCount = (items.count + sanitizedItemsPerPage - 1) / sanitizedItemsPerPage

        var pages: [PageInfo] = []

        for pageIndex in 0..<pageCount {
            let startIndex = pageIndex * sanitizedItemsPerPage
            let endIndex = min(startIndex + sanitizedItemsPerPage, items.count)
            let pageItems = Array(items[startIndex..<endIndex])
            
            let appCount = pageItems.filter { if case .app = $0 { return true } else { return false } }.count
            let folderCount = pageItems.filter { if case .folder = $0 { return true } else { return false } }.count
            let emptyCount = pageItems.filter { if case .empty = $0 { return true } else { return false } }.count
            
            let pageInfo = PageInfo(
                pageIndex: pageIndex,
                startIndex: startIndex,
                endIndex: endIndex,
                appCount: appCount,
                folderCount: folderCount,
                emptyCount: emptyCount
            )
            
            pages.append(pageInfo)
        }
        
        return pages
    }
    
    private func calculateCacheSize() {
        cacheLock.lock()
        let iconSize = iconCache.count
        let appInfoSize = appInfoCache.count
        let gridLayoutSize = gridLayoutCache.count
        cacheLock.unlock()
        cacheSize = iconSize + appInfoSize + gridLayoutSize
    }

    
    /// 获取性能统计
    var performanceStats: PerformanceStats {
        return PerformanceStats(cacheSize: cacheSize)
    }
}

// MARK: - 缓存键生成器

private struct CacheKeyGenerator {
    func generateIconKey(for appPath: String) -> String {
        return "icon_\(appPath.hashValue)"
    }
    
    func generateAppInfoKey(for appPath: String) -> String {
        return "appinfo_\(appPath.hashValue)"
    }
    
    func generateGridLayoutKey(for layoutKey: String) -> String {
        return "grid_\(layoutKey.hashValue)"
    }
}

// MARK: - 网格布局缓存数据结构

private struct GridLayoutCacheData {
    let totalItems: Int
    let itemsPerPage: Int
    let columns: Int
    let rows: Int
    let pageCount: Int
}

private struct PageInfo {
    let pageIndex: Int
    let startIndex: Int
    let endIndex: Int
    let appCount: Int
    let folderCount: Int
    let emptyCount: Int
}

// MARK: - 缓存统计信息

extension AppCacheManager {
    var cacheStatistics: CacheStatistics {
        return CacheStatistics(
            iconCacheSize: iconCache.count,
            appInfoCacheSize: appInfoCache.count,
            gridLayoutCacheSize: gridLayoutCache.count,
            totalCacheSize: cacheSize,
            isCacheValid: isCacheValid,
            lastUpdate: lastCacheUpdate
        )
    }
}

struct CacheStatistics {
    let iconCacheSize: Int
    let appInfoCacheSize: Int
    let gridLayoutCacheSize: Int
    let totalCacheSize: Int
    let isCacheValid: Bool
    let lastUpdate: Date
}

struct PerformanceStats {
    let cacheSize: Int
}

```

## /LaunchNext/AppIcon.icon/Assets/2.searchbar_unspecified_unspecified_automatic.svg

```svg path="/LaunchNext/AppIcon.icon/Assets/2.searchbar_unspecified_unspecified_automatic.svg" 
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
       "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024">
 <g/>
 <g id="searchbar">
  <path d="M281.392 192c-32.0685 0-58.6365 1.55676-84.2532 21.2065C174.099 230.867 160.348 258.819 160.348 288s13.7511 57.133 36.7905 74.7935C222.755 382.443 249.323 384 281.392 384h461.216c32.0684 0 58.6365-1.55676 84.2532-21.2065C849.901 345.133 863.652 317.181 863.652 288s-13.751-57.133-36.7905-74.7935C801.245 193.557 774.677 192 742.608 192H281.392z" opacity="0.3" style="opacity:0.3;"/>
 </g>
</svg>

```

## /LaunchNext/AppIcon.icon/Assets/3.magnifier_unspecified_unspecified_automatic.svg

```svg path="/LaunchNext/AppIcon.icon/Assets/3.magnifier_unspecified_unspecified_automatic.svg" 
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
       "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024">
 <g/>
 <g id="magnifier">
  <path d="M319.843 330.938l-22.6128-22.7663c5.56607-7.65732 8.83954-17.0698 8.83954-27.2837c0-26.1817-21.2855-47.4568-47.4673-47.4568c-26.1828 0-47.4683 21.2751-47.4683 47.4568c0 26.1932 21.2855 47.4672 47.4683 47.4672c9.73706 0 18.6726-2.99277 26.1222-8.0434l22.8194 22.8675c1.67712 1.67691 3.89865 2.47522 6.23712 2.47522c4.96204 0 8.51089-3.7504 8.51089-8.65381C322.292 334.694 321.446 332.541 319.843 330.938zM258.591 316.233c-19.4927 0-35.3334-15.851-35.3334-35.3448c0-19.4834 15.8407-35.3333 35.3334-35.3333c19.5157 0 35.3449 15.8499 35.3449 35.3333C293.936 300.382 278.107 316.233 258.591 316.233z" fill="#F0F0F0" style="fill:#F0F0F0;"/>
 </g>
</svg>

```

## /LaunchNext/AppIcon.icon/Assets/4.chiclet6_unspecified_unspecified_automatic.svg

```svg path="/LaunchNext/AppIcon.icon/Assets/4.chiclet6_unspecified_unspecified_automatic.svg" 
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
       "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024">
 <defs>
  <linearGradient id="SVGID_1_" x1="2203.14" y1="176.474" x2="2363.14" y2="176.474" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.0000003 1 -1 0.0000003 928.4728394 -1531.1361084)">
   <stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
   <stop offset="0" stop-color="#c8cac8" stop-opacity="1"/>
   <stop offset="1" stop-color="#abadab" stop-opacity="1"/>
  </linearGradient>
 </defs>
 <g/>
 <g id="chiclet6">
  <path d="M832 773.719c0 2.2254 0.0009766 4.4505-0.0131226 6.6759c-0.0107422 1.87445-0.0324097 3.74847-0.0831299 5.62231c-0.110718 4.08337-0.351013 8.20221-1.07697 12.2403c-0.736328 4.09644-1.93805 7.909-3.83258 11.6309c-1.862 3.65814-4.29517 7.00574-7.19769 9.90826s-6.25018 5.33563-9.90826 7.19769c-3.72192 1.89453-7.53448 3.09625-11.6309 3.83264c-4.03802 0.725891-8.15686 0.966186-12.2402 1.0769c-1.8739 0.0507812-3.74792 0.0723877-5.62238 0.0831909C778.169 832.001 775.944 832 773.719 832h-43.4375c-2.22546 0-4.4505 0.0009155-6.67596-0.0131226c-1.87439-0.0107422-3.74841-0.0324097-5.62231-0.0831909c-4.08337-0.110657-8.20221-0.351013-12.2402-1.07684c-4.09644-0.736389-7.909-1.93817-11.6309-3.83264c-3.65814-1.86206-7.0058-4.29517-9.90833-7.19769s-5.33563-6.25018-7.19769-9.90826c-1.89447-3.72198-3.09625-7.53455-3.83264-11.631c-0.72583-4.03802-0.966186-8.15686-1.07684-12.2402c-0.0508423-1.87384-0.0724487-3.74792-0.0831909-5.62231c-0.0140991-2.22546-0.0131226-4.4505-0.0131226-6.67596L672 730.281c0-2.2254-0.0009155-4.4505 0.0131226-6.6759c0.0108032-1.87439 0.0324097-3.74841 0.0831909-5.62231c0.110718-4.08337 0.351013-8.20221 1.0769-12.2403c0.736389-4.09637 1.93811-7.909 3.83264-11.6309c1.862-3.65808 4.29517-7.00574 7.19769-9.90826c2.90247-2.90253 6.25012-5.33569 9.90826-7.19769c3.72192-1.89453 7.53448-3.09625 11.6309-3.83258c4.03802-0.725952 8.15686-0.966248 12.2402-1.07697c1.8739-0.0507812 3.74792-0.0724487 5.62238-0.0831909c2.2254-0.0140381 4.4505-0.0130615 6.6759-0.0130615L773.719 672c2.22546 0 4.4505-0.0009766 6.67596 0.0131226c1.87439 0.0107422 3.74841 0.0324097 5.62231 0.0831909c4.08337 0.110718 8.20221 0.351013 12.2402 1.0769c4.09644 0.736328 7.909 1.93805 11.6309 3.83264c3.65814 1.862 7.00574 4.29511 9.90826 7.19763s5.33569 6.25018 7.19769 9.90833c1.89459 3.72186 3.09631 7.53448 3.83264 11.6309c0.725891 4.03802 0.966186 8.15686 1.0769 12.2402c0.0507812 1.8739 0.0724487 3.74792 0.0831909 5.62231C832.001 725.831 832 728.056 832 730.281V773.719z" fill="url(#SVGID_1_)" style="fill:url(#SVGID_1_);"/>
 </g>
</svg>

```

## /LaunchNext/AppIcon.icon/Assets/5.chiclet5_unspecified_unspecified_automatic.svg

```svg path="/LaunchNext/AppIcon.icon/Assets/5.chiclet5_unspecified_unspecified_automatic.svg" 
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
       "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024">
 <defs>
  <linearGradient id="SVGID_1_" x1="2203.14" y1="416.473" x2="2363.14" y2="416.473" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.0000003 1 -1 0.0000003 928.4728394 -1531.1361084)">
   <stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
   <stop offset="0" stop-color="#5dc0ff" stop-opacity="1"/>
   <stop offset="1" stop-color="#069eff" stop-opacity="1"/>
  </linearGradient>
 </defs>
 <g/>
 <g id="chiclet5">
  <path d="M592.001 773.719c0 2.2254 0.0009766 4.4505-0.0130615 6.6759c-0.0107422 1.87445-0.0324707 3.74847-0.0831909 5.62231c-0.110718 4.08337-0.351013 8.20221-1.0769 12.2403c-0.736328 4.09644-1.93811 7.909-3.83264 11.6309c-1.862 3.65814-4.29517 7.00574-7.19769 9.90826s-6.25012 5.33563-9.90826 7.19769c-3.72192 1.89453-7.53448 3.09625-11.6309 3.83264c-4.03802 0.725891-8.15686 0.966186-12.2402 1.0769c-1.8739 0.0507812-3.74792 0.0723877-5.62238 0.0831909C538.17 832.001 535.945 832 533.72 832h-43.4375c-2.22543 0-4.4505 0.0009155-6.67593-0.0131226c-1.87442-0.0107422-3.74847-0.0324097-5.62235-0.0831909c-4.08337-0.110657-8.20218-0.351013-12.2402-1.07684c-4.09644-0.736389-7.909-1.93817-11.6309-3.83264c-3.65814-1.86206-7.0058-4.29517-9.90829-7.19769c-2.90253-2.90253-5.33566-6.25018-7.19769-9.90826c-1.8945-3.72198-3.09625-7.53455-3.83264-11.631c-0.725891-4.03802-0.966217-8.15686-1.0769-12.2402c-0.0507812-1.87384-0.0723877-3.74792-0.0831604-5.62231c-0.0140686-2.22546-0.0131226-4.4505-0.0131226-6.67596l-3.05e-05-43.4375c0-2.2254-0.000946-4.4505 0.0131226-6.6759c0.0107727-1.87439 0.0323792-3.74841 0.0831909-5.62231c0.110657-4.08337 0.351013-8.20221 1.07687-12.2403c0.736389-4.09637 1.93811-7.909 3.83264-11.6309c1.86203-3.65808 4.29517-7.00574 7.19766-9.90826c2.90253-2.90253 6.25015-5.33569 9.90829-7.19769c3.72192-1.89453 7.53448-3.09625 11.6309-3.83258c4.03806-0.725952 8.15689-0.966248 12.2403-1.07697c1.87387-0.0507812 3.74789-0.0724487 5.62231-0.0831909c2.22543-0.0140381 4.4505-0.0130615 6.67593-0.0130615L533.72 672c2.22546 0 4.4505-0.0009766 6.67596 0.0131226c1.87439 0.0107422 3.74841 0.0324097 5.62231 0.0831909c4.08337 0.110718 8.20221 0.351013 12.2402 1.0769c4.09644 0.736328 7.90906 1.93805 11.6309 3.83264c3.65814 1.862 7.0058 4.29511 9.90833 7.19763s5.33563 6.25018 7.19763 9.90833c1.89459 3.72186 3.09631 7.53448 3.83264 11.6309c0.725952 4.03802 0.966248 8.15686 1.07697 12.2402c0.0507202 1.8739 0.0723877 3.74792 0.0831299 5.62231c0.0140991 2.22546 0.0131226 4.4505 0.0131226 6.6759V773.719z" fill="url(#SVGID_1_)" style="fill:url(#SVGID_1_);"/>
 </g>
</svg>

```

## /LaunchNext/AppIcon.icon/Assets/6.chiclet4_unspecified_unspecified_automatic.svg

```svg path="/LaunchNext/AppIcon.icon/Assets/6.chiclet4_unspecified_unspecified_automatic.svg" 
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
       "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024">
 <defs>
  <linearGradient id="SVGID_1_" x1="2203.14" y1="656.474" x2="2363.14" y2="656.474" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.0000003 1 -1 0.0000003 928.4728394 -1531.1361084)">
   <stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
   <stop offset="0" stop-color="#d599ff" stop-opacity="1"/>
   <stop offset="1" stop-color="#be62ff" stop-opacity="1"/>
  </linearGradient>
 </defs>
 <g/>
 <g id="chiclet4">
  <path d="M352 773.719c0 2.2254 0.0009766 4.4505-0.013092 6.6759c-0.0107422 1.87445-0.0324097 3.74847-0.0831604 5.62231c-0.110718 4.08337-0.351013 8.20221-1.07693 12.2403c-0.736328 4.09644-1.93808 7.909-3.83261 11.6309c-1.86203 3.65814-4.29517 7.00574-7.19769 9.90826s-6.25015 5.33563-9.90826 7.19769c-3.72192 1.89453-7.53452 3.09625-11.6309 3.83264c-4.03806 0.725891-8.15686 0.966186-12.2402 1.0769c-1.87393 0.0507812-3.74796 0.0723877-5.62235 0.0831909C298.169 832.001 295.944 832 293.719 832h-43.4375c-2.22543 0-4.45052 0.0009155-6.67593-0.0131226c-1.87444-0.0107422-3.74846-0.0324097-5.62233-0.0831909c-4.08339-0.110657-8.20221-0.351013-12.2403-1.07684c-4.09644-0.736389-7.90898-1.93817-11.6309-3.83264c-3.65814-1.86206-7.00578-4.29517-9.90829-7.19769c-2.90253-2.90253-5.33565-6.25018-7.19768-9.90826c-1.89452-3.72198-3.09627-7.53455-3.83264-11.631c-0.725891-4.03802-0.966217-8.15686-1.07689-12.2402c-0.0507965-1.87384-0.0724182-3.74792-0.0831909-5.62231c-0.0140686-2.22546-0.0131226-4.4505-0.0131226-6.67596L192 730.281c0-2.2254-0.000946-4.4505 0.0131226-6.6759c0.0107727-1.87439 0.0323792-3.74841 0.0831757-5.62231c0.110672-4.08337 0.351013-8.20221 1.07689-12.2403c0.736374-4.09637 1.93811-7.909 3.83263-11.6309c1.86205-3.65808 4.29517-7.00574 7.19768-9.90826s6.25015-5.33569 9.90829-7.19769c3.72192-1.89453 7.53448-3.09625 11.6309-3.83258c4.03806-0.725952 8.15688-0.966248 12.2402-1.07697c1.87387-0.0507812 3.74791-0.0724487 5.62233-0.0831909c2.22543-0.0140381 4.4505-0.0130615 6.67593-0.0130615L293.719 672c2.2254 0 4.45047-0.0009766 6.6759 0.0131226c1.87439 0.0107422 3.74844 0.0324097 5.62235 0.0831909c4.08337 0.110718 8.20221 0.351013 12.2402 1.0769c4.0964 0.736328 7.90903 1.93805 11.6309 3.83264c3.65811 1.862 7.00577 4.29511 9.90829 7.19763s5.33566 6.25018 7.19769 9.90833c1.89453 3.72186 3.09628 7.53448 3.83261 11.6309c0.725922 4.03802 0.966217 8.15686 1.07693 12.2402c0.0507507 1.8739 0.0724182 3.74792 0.0831604 5.62231c0.0140991 2.22546 0.0131226 4.4505 0.0131226 6.6759V773.719z" fill="url(#SVGID_1_)" style="fill:url(#SVGID_1_);"/>
 </g>
</svg>

```

## /LaunchNext/AppIcon.icon/Assets/7.chiclet3_unspecified_unspecified_automatic.svg

```svg path="/LaunchNext/AppIcon.icon/Assets/7.chiclet3_unspecified_unspecified_automatic.svg" 
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
       "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024">
 <defs>
  <linearGradient id="SVGID_1_" x1="1982.79" y1="176.473" x2="2142.79" y2="176.473" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.0000003 1 -1 0.0000003 928.4728394 -1531.1361084)">
   <stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
   <stop offset="0" stop-color="#78ebd0" stop-opacity="1"/>
   <stop offset="1" stop-color="#30e1b7" stop-opacity="1"/>
  </linearGradient>
 </defs>
 <g/>
 <g id="chiclet3">
  <path d="M832 553.373c0 2.2254 0.0009766 4.4505-0.0131226 6.6759c-0.0107422 1.87445-0.0324097 3.74847-0.0831299 5.62238c-0.110718 4.08337-0.351013 8.20221-1.07697 12.2402c-0.736328 4.09644-1.93805 7.909-3.83258 11.6309c-1.862 3.65814-4.29517 7.0058-7.19769 9.90826c-2.90253 2.90253-6.25018 5.33569-9.90826 7.19769c-3.72192 1.89453-7.53448 3.09625-11.6309 3.83264c-4.03802 0.725891-8.15686 0.966248-12.2402 1.0769c-1.8739 0.0507812-3.74792 0.0724487-5.62238 0.0831909c-2.2254 0.0140381-4.45044 0.0131226-6.6759 0.0131226h-43.4375c-2.22546 0-4.4505 0.0009766-6.67596-0.0131226c-1.87439-0.0107422-3.74841-0.0323486-5.62231-0.0831299c-4.08337-0.110718-8.20221-0.351013-12.2402-1.0769c-4.09644-0.736389-7.909-1.93811-11.6309-3.83264c-3.65814-1.86206-7.0058-4.29517-9.90833-7.19769s-5.33563-6.25012-7.19769-9.90826c-1.89447-3.72192-3.09625-7.53448-3.83264-11.6309c-0.72583-4.03809-0.966186-8.15686-1.07684-12.2403c-0.0508423-1.87384-0.0724487-3.74786-0.0831909-5.62231c-0.0140991-2.2254-0.0131226-4.4505-0.0131226-6.6759L672 509.936c0-2.22543-0.0009155-4.4505 0.0131226-6.6759c0.0108032-1.87442 0.0324097-3.74844 0.0831909-5.62235c0.110718-4.08337 0.351013-8.20221 1.0769-12.2403c0.736389-4.0964 1.93811-7.909 3.83264-11.6309c1.862-3.65811 4.29517-7.00574 7.19769-9.90826c2.90247-2.90253 6.25012-5.33566 9.90826-7.19769c3.72192-1.89453 7.53448-3.09628 11.6309-3.83261c4.03802-0.725922 8.15686-0.966217 12.2402-1.07693c1.8739-0.0507812 3.74792-0.0724487 5.62238-0.0831909c2.2254-0.0140686 4.4505-0.013092 6.6759-0.013092h43.4375c2.22546-3.05e-05 4.4505-0.0010071 6.67596 0.013092c1.87439 0.0107422 3.74841 0.0324097 5.62231 0.0831604c4.08337 0.110718 8.20221 0.351013 12.2402 1.07693c4.09644 0.736328 7.909 1.93805 11.6309 3.83261c3.65814 1.862 7.00574 4.29514 9.90826 7.19766s5.33569 6.25018 7.19769 9.90829c1.89459 3.72189 3.09631 7.53448 3.83264 11.6309c0.725891 4.03802 0.966186 8.15686 1.0769 12.2402c0.0507812 1.8739 0.0724487 3.74796 0.0831909 5.62235C832.001 505.485 832 507.71 832 509.936V553.373z" fill="url(#SVGID_1_)" style="fill:url(#SVGID_1_);"/>
 </g>
</svg>

```

## /LaunchNext/AppIcon.icon/Assets/8.chiclet2_unspecified_unspecified_automatic.svg

```svg path="/LaunchNext/AppIcon.icon/Assets/8.chiclet2_unspecified_unspecified_automatic.svg" 
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
       "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024">
 <defs>
  <linearGradient id="SVGID_1_" x1="1982.79" y1="416.474" x2="2142.79" y2="416.474" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.0000003 1 -1 0.0000003 928.4728394 -1531.1361084)">
   <stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
   <stop offset="0" stop-color="#ff70bb" stop-opacity="1"/>
   <stop offset="1" stop-color="#ff2396" stop-opacity="1"/>
  </linearGradient>
 </defs>
 <g/>
 <g id="chiclet2">
  <path d="M592 553.373c0 2.2254 0.0009766 4.4505-0.0131226 6.6759c-0.0107422 1.87445-0.0324097 3.74847-0.0831299 5.62238c-0.110718 4.08337-0.351013 8.20221-1.07697 12.2402c-0.736328 4.09644-1.93805 7.909-3.83258 11.6309c-1.86206 3.65814-4.29517 7.0058-7.19769 9.90826c-2.90253 2.90253-6.25018 5.33569-9.90826 7.19769c-3.72192 1.89453-7.53455 3.09625-11.6309 3.83264c-4.03809 0.725891-8.15686 0.966248-12.2402 1.0769c-1.87396 0.0507812-3.74799 0.0724487-5.62238 0.0831909c-2.2254 0.0140381-4.4505 0.0131226-6.6759 0.0131226h-43.4375c-2.22543 0-4.4505 0.0009766-6.67593-0.0131226c-1.87442-0.0107422-3.74844-0.0323486-5.62231-0.0831299c-4.08337-0.110718-8.20221-0.351013-12.2403-1.0769c-4.09644-0.736389-7.909-1.93811-11.6309-3.83264c-3.65814-1.86206-7.00577-4.29517-9.90829-7.19769s-5.33563-6.25012-7.19769-9.90826c-1.8945-3.72192-3.09625-7.53448-3.83264-11.6309c-0.725861-4.03809-0.966217-8.15686-1.07687-12.2403c-0.0508118-1.87384-0.0724182-3.74786-0.0831909-5.62231c-0.0140686-2.2254-0.0131226-4.4505-0.0131226-6.6759L432 509.936c0-2.22543-0.0009155-4.4505 0.0131226-6.6759c0.0107727-1.87442 0.0324097-3.74844 0.0831909-5.62235c0.110687-4.08337 0.351013-8.20221 1.07687-12.2403c0.736389-4.0964 1.93814-7.909 3.83264-11.6309c1.86203-3.65811 4.29517-7.00574 7.19769-9.90826c2.9025-2.90253 6.25012-5.33566 9.90826-7.19769c3.72195-1.89453 7.53452-3.09628 11.6309-3.83261c4.03806-0.725922 8.15689-0.966217 12.2403-1.07693c1.87387-0.0507812 3.74789-0.0724487 5.62235-0.0831909c2.22543-0.0140686 4.4505-0.013092 6.67593-0.013092h43.4375c2.2254-3.05e-05 4.4505-0.0010071 6.6759 0.013092c1.87439 0.0107422 3.74847 0.0324097 5.62238 0.0831604c4.08337 0.110718 8.20221 0.351013 12.2402 1.07693c4.09637 0.736328 7.909 1.93805 11.6309 3.83261c3.65808 1.862 7.00574 4.29514 9.90826 7.19766s5.33569 6.25018 7.19769 9.90829c1.89453 3.72189 3.09631 7.53448 3.83264 11.6309c0.725891 4.03802 0.966186 8.15686 1.0769 12.2402c0.0507812 1.8739 0.0724487 3.74796 0.0831909 5.62235C592.001 505.485 592 507.71 592 509.936V553.373z" fill="url(#SVGID_1_)" style="fill:url(#SVGID_1_);"/>
 </g>
</svg>

```

## /LaunchNext/AppIcon.icon/Assets/9.chiclet1_unspecified_unspecified_automatic.svg

```svg path="/LaunchNext/AppIcon.icon/Assets/9.chiclet1_unspecified_unspecified_automatic.svg" 
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
       "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024">
 <defs>
  <linearGradient id="SVGID_1_" x1="1982.79" y1="656.474" x2="2142.79" y2="656.474" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.0000003 1 -1 0.0000003 928.4728394 -1531.1361084)">
   <stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
   <stop offset="0" stop-color="#ffa759" stop-opacity="1"/>
   <stop offset="1" stop-color="#ff7700" stop-opacity="1"/>
  </linearGradient>
 </defs>
 <g/>
 <g id="chiclet1">
  <path d="M352 553.373c0 2.2254 0.0009766 4.4505-0.013092 6.6759c-0.0107422 1.87445-0.0324097 3.74847-0.0831604 5.62238c-0.110718 4.08337-0.351013 8.20221-1.07693 12.2402c-0.736328 4.09644-1.93808 7.909-3.83261 11.6309c-1.86203 3.65814-4.29517 7.0058-7.19769 9.90826c-2.90253 2.90253-6.25015 5.33569-9.90826 7.19769c-3.72192 1.89453-7.53452 3.09625-11.6309 3.83264c-4.03806 0.725891-8.15686 0.966248-12.2402 1.0769c-1.87393 0.0507812-3.74796 0.0724487-5.62235 0.0831909c-2.22543 0.0140381-4.4505 0.0131226-6.67593 0.0131226h-43.4375c-2.22543 0-4.45052 0.0009766-6.67593-0.0131226c-1.87444-0.0107422-3.74846-0.0323486-5.62233-0.0831299c-4.08339-0.110718-8.20221-0.351013-12.2403-1.0769c-4.09644-0.736389-7.90898-1.93811-11.6309-3.83264c-3.65814-1.86206-7.00578-4.29517-9.90829-7.19769c-2.90253-2.90253-5.33565-6.25012-7.19768-9.90826c-1.89452-3.72192-3.09627-7.53448-3.83264-11.6309c-0.725891-4.03809-0.966217-8.15686-1.07689-12.2403c-0.0507965-1.87384-0.0724182-3.74786-0.0831909-5.62231c-0.0140686-2.2254-0.0131226-4.4505-0.0131226-6.6759L192 509.936c0-2.22543-0.000946-4.4505 0.0131226-6.6759c0.0107727-1.87442 0.0323792-3.74844 0.0831757-5.62235c0.110672-4.08337 0.351013-8.20221 1.07689-12.2403c0.736374-4.0964 1.93811-7.909 3.83263-11.6309c1.86205-3.65811 4.29517-7.00574 7.19768-9.90826s6.25015-5.33566 9.90829-7.19769c3.72192-1.89453 7.53448-3.09628 11.6309-3.83261c4.03806-0.725922 8.15688-0.966217 12.2402-1.07693c1.87387-0.0507812 3.74791-0.0724487 5.62233-0.0831909c2.22543-0.0140686 4.4505-0.013092 6.67593-0.013092H293.719c2.2254-3.05e-05 4.45047-0.0010071 6.6759 0.013092c1.87439 0.0107422 3.74844 0.0324097 5.62235 0.0831604c4.08337 0.110718 8.20221 0.351013 12.2402 1.07693c4.0964 0.736328 7.90903 1.93805 11.6309 3.83261c3.65811 1.862 7.00577 4.29514 9.90829 7.19766s5.33566 6.25018 7.19769 9.90829c1.89453 3.72189 3.09628 7.53448 3.83261 11.6309c0.725922 4.03802 0.966217 8.15686 1.07693 12.2402c0.0507507 1.8739 0.0724182 3.74796 0.0831604 5.62235c0.0140991 2.22543 0.0131226 4.45047 0.0131226 6.6759V553.373z" fill="url(#SVGID_1_)" style="fill:url(#SVGID_1_);"/>
 </g>
</svg>

```

## /LaunchNext/AppIcon.icon/icon.json

```json path="/LaunchNext/AppIcon.icon/icon.json" 
{
  "fill-specializations" : [
    {
      "value" : "automatic"
    },
    {
      "appearance" : "dark",
      "value" : "automatic"
    }
  ],
  "groups" : [
    {
      "blur-material" : null,
      "layers" : [
        {
          "image-name" : "9.chiclet1_unspecified_unspecified_automatic.svg",
          "name" : "9.chiclet1_unspecified_unspecified_automatic"
        },
        {
          "image-name" : "8.chiclet2_unspecified_unspecified_automatic.svg",
          "name" : "8.chiclet2_unspecified_unspecified_automatic"
        },
        {
          "image-name" : "7.chiclet3_unspecified_unspecified_automatic.svg",
          "name" : "7.chiclet3_unspecified_unspecified_automatic"
        },
        {
          "image-name" : "6.chiclet4_unspecified_unspecified_automatic.svg",
          "name" : "6.chiclet4_unspecified_unspecified_automatic"
        },
        {
          "image-name" : "5.chiclet5_unspecified_unspecified_automatic.svg",
          "name" : "5.chiclet5_unspecified_unspecified_automatic"
        },
        {
          "image-name" : "4.chiclet6_unspecified_unspecified_automatic.svg",
          "name" : "4.chiclet6_unspecified_unspecified_automatic"
        },
        {
          "image-name" : "3.magnifier_unspecified_unspecified_automatic.svg",
          "name" : "3.magnifier_unspecified_unspecified_automatic"
        },
        {
          "image-name" : "2.searchbar_unspecified_unspecified_automatic.svg",
          "name" : "2.searchbar_unspecified_unspecified_automatic"
        }
      ],
      "lighting" : "individual",
      "shadow" : {
        "kind" : "neutral",
        "opacity" : 0.5
      },
      "specular" : true,
      "translucency" : {
        "enabled" : true,
        "value" : 0.5
      }
    }
  ],
  "supported-platforms" : {
    "squares" : "shared"
  }
}
```

## /LaunchNext/AppInfo.swift

```swift path="/LaunchNext/AppInfo.swift" 
import Foundation
import AppKit
import CoreServices

struct AppInfo: Identifiable, Equatable, Hashable {
    let name: String
    let icon: NSImage
    let url: URL

    static let transparentPlaceholderIcon: NSImage = {
        let size = NSSize(width: 1, height: 1)
        let image = NSImage(size: size)
        image.lockFocus()
        NSColor.clear.setFill()
        NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
        image.unlockFocus()
        image.isTemplate = false
        return image
    }()

    // 使用应用路径作为稳定唯一标识
    var id: String { url.path }

    static func == (lhs: AppInfo, rhs: AppInfo) -> Bool {
        lhs.url == rhs.url
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(url.path)
    }

    // MARK: - 创建 AppInfo
    static func from(url: URL, preferredName: String? = nil, customTitle: String? = nil, loadIcon: Bool = true) -> AppInfo {
        let fallbackName = normalizeCandidate(url.deletingPathExtension().lastPathComponent)
        let bundle = Bundle(url: url)
        let localizedName = localizedAppName(for: url,
                                             preferredName: preferredName,
                                             fallbackName: fallbackName,
                                             bundle: bundle)
        let englishName = englishAppName(preferredName: preferredName,
                                         fallbackName: fallbackName,
                                         bundle: bundle)

        let shouldUseLocalized = shouldUseLocalizedTitles()
        let chosenName = shouldUseLocalized ? localizedName : englishName
        let icon: NSImage
        if loadIcon {
            icon = NSWorkspace.shared.icon(forFile: url.path)
        } else {
            icon = transparentPlaceholderIcon
        }

        if let override = customTitle.flatMap({ title -> String? in
            let normalized = normalizeCandidate(title)
            return normalized.isEmpty ? nil : normalized
        }) {
            return AppInfo(name: override, icon: icon, url: url)
        }

        return AppInfo(name: chosenName, icon: icon, url: url)
    }

    // MARK: - 获取本地化应用名
    private static func localizedAppName(for url: URL,
                                         preferredName: String?,
                                         fallbackName: String,
                                         bundle: Bundle?) -> String {
        var resolvedName: String? = nil

        func consider(_ rawValue: String?, source: String) {
            guard let rawValue = rawValue else {
                return
            }
            let normalized = normalizeCandidate(rawValue)
            if normalized.isEmpty {
                return
            }
            guard resolvedName == nil else { return }
            if normalized != fallbackName {
                resolvedName = normalized
            }
        }

        if let bundle {
            consider(bundlePreferredDisplayName(bundle), source: "BundlePreferredDisplayName")
        }

        return resolvedName ?? fallbackName
    }

    private static func englishAppName(preferredName: String?,
                                       fallbackName: String,
                                       bundle: Bundle?) -> String {
        var candidates: [String] = []

        if let bundle {
            let englishLocales = ["en", "en-US", "en-GB"]
            for locale in englishLocales {
                if let path = bundle.path(forResource: "InfoPlist",
                                           ofType: "strings",
                                           inDirectory: nil,
                                           forLocalization: locale),
                   let dict = NSDictionary(contentsOfFile: path) as? [String: String] {
                    for key in ["CFBundleDisplayName", "CFBundleName"] {
                        if let value = dict[key], !value.isEmpty {
                            candidates.append(value)
                        }
                    }
                }
            }

            for key in ["CFBundleDisplayName", "CFBundleName"] {
                if let value = bundle.infoDictionary?[key] as? String, !value.isEmpty {
                    candidates.append(value)
                }
            }
        }

        if let preferredName, !preferredName.isEmpty {
            candidates.append(preferredName)
        }

        for raw in candidates {
            let normalized = normalizeCandidate(raw)
            if !normalized.isEmpty {
                return normalized
            }
        }

        return fallbackName
    }

    private static func shouldUseLocalizedTitles() -> Bool {
        let defaults = UserDefaults.standard
        if defaults.object(forKey: "useLocalizedThirdPartyTitles") == nil {
            return true
        }
        return defaults.bool(forKey: "useLocalizedThirdPartyTitles")
    }

    private static func normalizeCandidate(_ value: String) -> String {
        var trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.lowercased().hasSuffix(".app") {
            trimmed = String(trimmed.dropLast(4))
        }
        return trimmed
    }

    private static func bundlePreferredDisplayName(_ bundle: Bundle) -> String? {
        let preferredLocales = Bundle.preferredLocalizations(from: bundle.localizations, forPreferences: userPreferredLanguages())
        if let chosen = preferredLocales.first,
           let lprojPath = bundle.path(forResource: chosen, ofType: "lproj"),
           let localizedBundle = Bundle(path: lprojPath),
           let stringsPath = localizedBundle.path(forResource: "InfoPlist", ofType: "strings"),
           let dict = NSDictionary(contentsOfFile: stringsPath) as? [String: String] {
            if let displayName = dict["CFBundleDisplayName"], !displayName.isEmpty {
                return displayName
            }
            if let bundleName = dict["CFBundleName"], !bundleName.isEmpty {
                return bundleName
            }
        }

        if let localizedInfo = bundle.localizedInfoDictionary {
            if let displayName = localizedInfo["CFBundleDisplayName"] as? String, !displayName.isEmpty {
                return displayName
            }
            if let bundleName = localizedInfo["CFBundleName"] as? String, !bundleName.isEmpty {
                return bundleName
            }
        }

        return nil
    }

    private static func userPreferredLanguages() -> [String] {
        if let languages = UserDefaults.standard.array(forKey: "AppleLanguages") as? [String], !languages.isEmpty {
            return languages
        }
        return Locale.preferredLanguages
    }
}

```

## /LaunchNext/Assets.xcassets/AboutBackground.imageset/Contents.json

```json path="/LaunchNext/Assets.xcassets/AboutBackground.imageset/Contents.json" 
{
  "images" : [
    {
      "filename" : "background_light.heic",
      "idiom" : "universal"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "filename" : "background_dark.heic",
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

```

## /LaunchNext/Assets.xcassets/AboutBackground.imageset/background_dark.heic

Binary file available at https://raw.githubusercontent.com/RoversX/LaunchNext/refs/heads/main/LaunchNext/Assets.xcassets/AboutBackground.imageset/background_dark.heic

## /LaunchNext/Assets.xcassets/AboutBackground.imageset/background_light.heic

Binary file available at https://raw.githubusercontent.com/RoversX/LaunchNext/refs/heads/main/LaunchNext/Assets.xcassets/AboutBackground.imageset/background_light.heic

## /LaunchNext/Assets.xcassets/AccentColor.colorset/Contents.json

```json path="/LaunchNext/Assets.xcassets/AccentColor.colorset/Contents.json" 
{
  "colors" : [
    {
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

```

## /LaunchNext/Assets.xcassets/AppIcon.appiconset/Contents.json

```json path="/LaunchNext/Assets.xcassets/AppIcon.appiconset/Contents.json" 
{
  "images" : [
    {
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "16x16"
    },
    {
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "16x16"
    },
    {
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "32x32"
    },
    {
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "32x32"
    },
    {
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "128x128"
    },
    {
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "128x128"
    },
    {
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "256x256"
    },
    {
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "256x256"
    },
    {
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "512x512"
    },
    {
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "512x512"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

```

## /LaunchNext/Assets.xcassets/AppearanceAuto.imageset/Contents.json

```json path="/LaunchNext/Assets.xcassets/AppearanceAuto.imageset/Contents.json" 
{
  "images" : [
    {
      "filename" : "auto_appearance_mode.heic",
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

```

## /LaunchNext/Assets.xcassets/AppearanceAuto.imageset/auto_appearance_mode.heic

Binary file available at https://raw.githubusercontent.com/RoversX/LaunchNext/refs/heads/main/LaunchNext/Assets.xcassets/AppearanceAuto.imageset/auto_appearance_mode.heic

## /LaunchNext/Assets.xcassets/AppearanceDark.imageset/Contents.json

```json path="/LaunchNext/Assets.xcassets/AppearanceDark.imageset/Contents.json" 
{
  "images" : [
    {
      "filename" : "dark_appearance_mode.heic",
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

```

## /LaunchNext/Assets.xcassets/AppearanceDark.imageset/dark_appearance_mode.heic

Binary file available at https://raw.githubusercontent.com/RoversX/LaunchNext/refs/heads/main/LaunchNext/Assets.xcassets/AppearanceDark.imageset/dark_appearance_mode.heic

## /LaunchNext/Assets.xcassets/AppearanceLight.imageset/Contents.json

```json path="/LaunchNext/Assets.xcassets/AppearanceLight.imageset/Contents.json" 
{
  "images" : [
    {
      "filename" : "light_appearance_mode.heic",
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

```

## /LaunchNext/Assets.xcassets/AppearanceLight.imageset/light_appearance_mode.heic

Binary file available at https://raw.githubusercontent.com/RoversX/LaunchNext/refs/heads/main/LaunchNext/Assets.xcassets/AppearanceLight.imageset/light_appearance_mode.heic

## /LaunchNext/Assets.xcassets/Contents.json

```json path="/LaunchNext/Assets.xcassets/Contents.json" 
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

```

## /LaunchNext/Assets.xcassets/FoundaryColor.colorset/Contents.json

```json path="/LaunchNext/Assets.xcassets/FoundaryColor.colorset/Contents.json" 
{
  "colors" : [
    {
      "color" : {
        "color-space" : "display-p3",
        "components" : {
          "alpha" : "1.000",
          "blue" : "1.000",
          "green" : "1.000",
          "red" : "1.000"
        }
      },
      "idiom" : "universal"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "light"
        }
      ],
      "color" : {
        "color-space" : "display-p3",
        "components" : {
          "alpha" : "1.000",
          "blue" : "1.000",
          "green" : "1.000",
          "red" : "1.000"
        }
      },
      "idiom" : "universal"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "color" : {
        "color-space" : "display-p3",
        "components" : {
          "alpha" : "0.700",
          "blue" : "0.400",
          "green" : "0.400",
          "red" : "0.400"
        }
      },
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

```

## /LaunchNext/CAFolderGridView.swift

```swift path="/LaunchNext/CAFolderGridView.swift" 
import AppKit
import QuartzCore

final class CAFolderGridView: NSView {
    var apps: [AppInfo] = [] {
        didSet { rebuildLayers() }
    }
    var layoutMode: AppStore.FolderLayoutMode = .paged {
        didSet {
            guard layoutMode != oldValue else { return }
            updateLayerClipping()
            currentPage = min(currentPage, max(pageCount - 1, 0))
            verticalOffset = 0
            horizontalOffset = pageOffset(for: currentPage, metrics: makeMetrics())
            targetHorizontalOffset = horizontalOffset
            updateLayout(animated: false)
        }
    }
    var iconSize: CGFloat = 72 {
        didSet {
            guard iconSize != oldValue else { return }
            clearIconCache()
            rebuildLayers()
        }
    }
    var labelFontSize: CGFloat = 12 {
        didSet {
            guard labelFontSize != oldValue else { return }
            updateLabelFonts()
            updateLayout(animated: false)
        }
    }
    var labelFontWeight: NSFont.Weight = .medium {
        didSet {
            guard labelFontWeight != oldValue else { return }
            updateLabelFonts()
        }
    }
    var showLabels: Bool = true {
        didSet {
            guard showLabels != oldValue else { return }
            updateLabelVisibility()
            updateLayout(animated: false)
        }
    }
    var hoverMagnificationEnabled: Bool = false {
        didSet {
            guard hoverMagnificationEnabled != oldValue else { return }
            if !hoverMagnificationEnabled { updateHoverIndex(nil) }
        }
    }
    var hoverMagnificationScale: CGFloat = 1.2
    var activePressEffectEnabled: Bool = false
    var activePressScale: CGFloat = 0.92
    var animationsEnabled: Bool = true
    var animationDuration: Double = 0.3
    var isLayoutLocked: Bool = false
    var scrollSensitivity: Double = AppStore.defaultScrollSensitivity
    var reverseWheelPagingDirection: Bool = false
    var verticalHeaderHeight: CGFloat = 0 {
        didSet {
            guard verticalHeaderHeight != oldValue else { return }
            updateLayout(animated: false)
        }
    }

    var showInFinderMenuTitle: String = "Show in Finder"
    var copyAppPathMenuTitle: String = "Copy App Path"
    var hideAppMenuTitle: String = "Hide application"
    var uninstallWithToolMenuTitle: String = "Uninstall with configured tool"
    var canUseConfiguredUninstallTool: Bool = false
    var contextMenuTargetApp: AppInfo?

    var onOpenApp: ((AppInfo) -> Void)?
    var onReorderApps: ((Int, Int) -> Void)?
    var onDragAppOut: ((AppInfo) -> Void)?
    var onShowAppInFinder: ((AppInfo) -> Void)?
    var onCopyAppPath: ((AppInfo) -> Void)?
    var onHideApp: ((AppInfo) -> Void)?
    var onUninstallWithTool: ((AppInfo) -> Void)?
    var onClose: (() -> Void)?
    var onPageStateChanged: ((Int, Int) -> Void)?
    var onVerticalScrollOffsetChanged: ((CGFloat) -> Void)?

    private let baseContentInsets = NSEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
    private var contentInsets: NSEdgeInsets {
        var insets = baseContentInsets
        if layoutMode == .vertical {
            insets.top += verticalHeaderHeight
        }
        return insets
    }
    private let columnSpacing: CGFloat = 22
    private let rowSpacing: CGFloat = 18
    private let dragOutInset: CGFloat = -14
    private let pageFlipEdgeWidth: CGFloat = 60
    private let pageFlipDelay: TimeInterval = 0.4

    private var contentLayer = CALayer()
    private var displayLink: CADisplayLink?
    private var appLayers: [CALayer] = []
    private var itemFrames: [CGRect] = []
    private var iconCache: [String: CGImage] = [:]
    private let iconCacheLock = NSLock()
    private var hoverTrackingArea: NSTrackingArea?
    private var currentPage = 0
    private var horizontalOffset: CGFloat = 0
    private var targetHorizontalOffset: CGFloat = 0
    private var verticalOffset: CGFloat = 0
    private var pageCount: Int {
        let metrics = makeMetrics()
        return max(1, (apps.count + metrics.itemsPerPage - 1) / metrics.itemsPerPage)
    }
    private var selectedIndex: Int?
    private var hoveredIndex: Int?
    private var pressedIndex: Int?
    private var dragStartPoint: CGPoint = .zero
    private var draggingIndex: Int?
    private var draggingApp: AppInfo?
    private var draggingLayer: CALayer?
    private var isDraggingItem = false
    private var dragCurrentPoint: CGPoint = .zero
    private var edgeDragTimer: Timer?
    private var edgeDragDirection: Int?
    private var edgeDragRequiresReentry = false
    private var pendingDragUpdateAfterPageAnimation = false
    private var isPageScrollDragging = false
    private var isPageScrollAnimating = false
    private var pageScrollStartOffset: CGFloat = 0
    private var pageScrollAccumulatedDelta: CGFloat = 0
    private var pageScrollSnapWorkItem: DispatchWorkItem?
    private var wheelAccumulatedDelta: CGFloat = 0
    private var wheelLastDirection = 0
    private var wheelLastFlipAt: Date?
    private let wheelFlipCooldown: TimeInterval = 0.15
    private var currentHoverIndex: Int?
    private var lastReportedPage: Int?
    private var lastReportedPageCount: Int?
    private var lastReportedVerticalOffset: CGFloat?

    var displayedPage: Int { currentPage }
    var displayedPageCount: Int { pageCount }

    func setDisplayedPage(_ page: Int, animated: Bool) {
        navigateToPage(page, animated: animated)
    }

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        setup()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }

    deinit {
        if let tracking = hoverTrackingArea {
            removeTrackingArea(tracking)
        }
        displayLink?.invalidate()
        edgeDragTimer?.invalidate()
        pageScrollSnapWorkItem?.cancel()
    }

    private func setup() {
        wantsLayer = true
        updateLayerClipping()
        contentLayer.masksToBounds = false
        layer?.addSublayer(contentLayer)
    }

    private func updateLayerClipping() {
        layer?.masksToBounds = false
    }

    override var acceptsFirstResponder: Bool { true }

    override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }

    override func layout() {
        super.layout()
        if layoutMode == .paged, !isPageScrollDragging, !isPageScrollAnimating, !isDraggingItem {
            let metrics = makeMetrics()
            horizontalOffset = pageOffset(for: currentPage, metrics: metrics)
            targetHorizontalOffset = horizontalOffset
        }
        updateLayout(animated: false)
    }

    override func updateTrackingAreas() {
        super.updateTrackingAreas()
        if let tracking = hoverTrackingArea {
            removeTrackingArea(tracking)
        }
        let options: NSTrackingArea.Options = [.mouseMoved, .mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect]
        let tracking = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
        addTrackingArea(tracking)
        hoverTrackingArea = tracking
    }

    override func viewDidMoveToWindow() {
        super.viewDidMoveToWindow()
        window?.acceptsMouseMovedEvents = true
        if window != nil {
            setupDisplayLinkIfNeeded()
        } else {
            displayLink?.invalidate()
            displayLink = nil
        }
        DispatchQueue.main.async { [weak self] in
            guard let self, let window = self.window else { return }
            window.makeFirstResponder(self)
        }
    }

    private func setupDisplayLinkIfNeeded() {
        guard displayLink == nil, let window else { return }
        displayLink = window.displayLink(target: self, selector: #selector(displayLinkFired(_:)))
        displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60, maximum: 120, preferred: 120)
        displayLink?.add(to: .main, forMode: .common)
    }

    @objc private func displayLinkFired(_ link: CADisplayLink) {
        guard isPageScrollAnimating else { return }
        updatePageScrollAnimation()
    }

    func updateSelection(_ index: Int?, animated: Bool = true) {
        let clamped = index.flatMap { apps.indices.contains($0) ? $0 : nil }
        guard selectedIndex != clamped else { return }
        let old = selectedIndex
        selectedIndex = clamped
        if let old { applyScale(at: old, animated: animated) }
        if let clamped { applyScale(at: clamped, animated: animated) }
        ensureSelectionVisible()
    }

    func clearIconCache() {
        iconCacheLock.lock()
        iconCache.removeAll()
        iconCacheLock.unlock()
    }

    private struct Metrics {
        var columns: Int
        var rows: Int
        var itemsPerPage: Int
        var cellWidth: CGFloat
        var cellHeight: CGFloat
        var contentHeight: CGFloat
        var totalItemHeight: CGFloat
        var labelHeight: CGFloat
        var labelTopSpacing: CGFloat
        var pageStride: CGFloat
    }

    private func makeMetrics() -> Metrics {
        let width = max(bounds.width, 1)
        let height = max(bounds.height, 1)
        let availableWidth = max(1, width - contentInsets.left - contentInsets.right)
        let availableHeight = max(1, height - contentInsets.top - contentInsets.bottom)
        let labelHeight: CGFloat = showLabels ? labelFontSize + 8 : 0
        let labelTopSpacing: CGFloat = showLabels ? 6 : 0
        let totalItemHeight = iconSize + labelTopSpacing + labelHeight
        let minCellWidth = max(iconSize + 18, iconSize * 1.32)
        let minCellHeight = max(totalItemHeight + 12, iconSize * 1.28)
        let columns = max(1, min(8, Int((availableWidth + columnSpacing) / (minCellWidth + columnSpacing))))
        let usableWidth = max(1, availableWidth - CGFloat(columns - 1) * columnSpacing)
        let cellWidth = usableWidth / CGFloat(columns)

        if layoutMode == .paged {
            let rows = max(1, min(5, Int((availableHeight + rowSpacing) / (minCellHeight + rowSpacing))))
            let usableHeight = max(1, availableHeight - CGFloat(rows - 1) * rowSpacing)
            let cellHeight = usableHeight / CGFloat(rows)
            return Metrics(columns: columns,
                           rows: rows,
                           itemsPerPage: max(1, columns * rows),
                           cellWidth: cellWidth,
                           cellHeight: cellHeight,
                           contentHeight: height,
                           totalItemHeight: totalItemHeight,
                           labelHeight: labelHeight,
                           labelTopSpacing: labelTopSpacing,
                           pageStride: width)
        }

        let rows = max(1, Int(ceil(Double(apps.count) / Double(max(columns, 1)))))
        let cellHeight = minCellHeight
        let contentHeight = contentInsets.top + contentInsets.bottom + CGFloat(rows) * cellHeight + CGFloat(max(rows - 1, 0)) * rowSpacing
        return Metrics(columns: columns,
                       rows: rows,
                       itemsPerPage: max(1, columns * max(rows, 1)),
                       cellWidth: cellWidth,
                       cellHeight: cellHeight,
                       contentHeight: max(height, contentHeight),
                       totalItemHeight: totalItemHeight,
                       labelHeight: labelHeight,
                       labelTopSpacing: labelTopSpacing,
                       pageStride: width)
    }

    private func rebuildLayers() {
        appLayers.forEach { $0.removeFromSuperlayer() }
        appLayers = apps.map { makeAppLayer(for: $0) }
        appLayers.forEach { contentLayer.addSublayer($0) }
        if selectedIndex.map({ !apps.indices.contains($0) }) ?? false {
            selectedIndex = apps.indices.first
        }
        updateLayout(animated: false)
    }

    private func makeAppLayer(for app: AppInfo) -> CALayer {
        let container = CALayer()
        container.masksToBounds = false
        container.contentsScale = backingScale

        let iconLayer = CALayer()
        iconLayer.name = "icon"
        iconLayer.contentsGravity = .resizeAspect
        iconLayer.contentsScale = backingScale
        iconLayer.masksToBounds = false
        iconLayer.shouldRasterize = true
        iconLayer.rasterizationScale = backingScale
        container.addSublayer(iconLayer)

        let textLayer = CATextLayer()
        textLayer.name = "label"
        textLayer.contentsScale = backingScale
        textLayer.alignmentMode = .center
        textLayer.truncationMode = .end
        textLayer.isWrapped = false
        textLayer.fontSize = labelFontSize
        textLayer.font = NSFont.systemFont(ofSize: labelFontSize, weight: labelFontWeight)
        textLayer.foregroundColor = currentLabelColor().cgColor
        textLayer.string = app.name
        textLayer.shouldRasterize = true
        textLayer.rasterizationScale = backingScale
        textLayer.isHidden = !showLabels
        container.addSublayer(textLayer)

        let warningLayer = CATextLayer()
        warningLayer.name = "missingWarning"
        warningLayer.contentsScale = backingScale
        warningLayer.alignmentMode = .center
        warningLayer.fontSize = max(10, iconSize * 0.13)
        warningLayer.font = NSFont.systemFont(ofSize: max(10, iconSize * 0.13), weight: .bold)
        warningLayer.foregroundColor = NSColor.white.cgColor
        warningLayer.backgroundColor = NSColor.systemOrange.cgColor
        warningLayer.cornerRadius = max(7, iconSize * 0.11)
        warningLayer.masksToBounds = true
        warningLayer.string = "!"
        warningLayer.isHidden = FileManager.default.fileExists(atPath: app.url.path)
        container.addSublayer(warningLayer)

        setIcon(for: iconLayer, app: app)
        return container
    }

    private var backingScale: CGFloat {
        window?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2
    }

    private func updateLayout(animated: Bool) {
        guard bounds.width > 0, bounds.height > 0 else { return }
        let metrics = makeMetrics()
        currentPage = min(currentPage, max(pageCount - 1, 0))
        if layoutMode == .paged {
            targetHorizontalOffset = clampHorizontalOffset(pageOffset(for: currentPage, metrics: metrics), metrics: metrics)
            horizontalOffset = clampHorizontalOffset(horizontalOffset, metrics: metrics)
            if !isPageScrollDragging, !isPageScrollAnimating, !isDraggingItem {
                horizontalOffset = targetHorizontalOffset
            }
            verticalOffset = 0
        } else {
            horizontalOffset = 0
            targetHorizontalOffset = 0
            verticalOffset = clampVerticalOffset(verticalOffset, metrics: metrics)
        }

        CATransaction.begin()
        CATransaction.setDisableActions(true)
        CATransaction.setAnimationDuration(0)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeOut))

        if layoutMode == .paged {
            let totalWidth = CGFloat(max(pageCount, 1)) * metrics.pageStride
            // Keep frame updates out of a transformed coordinate space; otherwise CA can leave the page container a few percent off.
            contentLayer.transform = CATransform3DIdentity
            contentLayer.frame = CGRect(x: 0, y: 0, width: totalWidth, height: bounds.height)
            contentLayer.transform = CATransform3DMakeTranslation(horizontalOffset, 0, 0)
        } else {
            contentLayer.transform = CATransform3DIdentity
            contentLayer.frame = bounds
        }

        itemFrames = Array(repeating: .zero, count: apps.count)
        for (index, layer) in appLayers.enumerated() {
            guard index < apps.count else { continue }
            let frame = frameForItem(at: index, metrics: metrics)
            itemFrames[index] = visibleFrame(frame)
            layer.transform = CATransform3DIdentity
            layer.frame = frame
            layoutSublayers(of: layer, metrics: metrics)
            layer.opacity = draggingIndex == index ? 0 : 1
        }
        CATransaction.commit()
        notifyPageStateChanged()
        notifyVerticalScrollOffsetChanged()
    }

    private func frameForItem(at index: Int, metrics: Metrics) -> CGRect {
        frameForGridSlot(at: index, metrics: metrics)
    }

    private func frameForGridSlot(at index: Int, metrics: Metrics) -> CGRect {
        let pageIndex: Int
        let localIndex: Int
        let xOffset: CGFloat
        if layoutMode == .paged {
            pageIndex = index / metrics.itemsPerPage
            localIndex = index % metrics.itemsPerPage
            xOffset = CGFloat(pageIndex) * metrics.pageStride
        } else {
            pageIndex = 0
            localIndex = index
            xOffset = 0
        }
        _ = pageIndex
        let col = localIndex % metrics.columns
        let row = localIndex / metrics.columns
        let x = contentInsets.left + xOffset + CGFloat(col) * (metrics.cellWidth + columnSpacing)
        let topBasedY = bounds.height - contentInsets.top - CGFloat(row + 1) * metrics.cellHeight - CGFloat(row) * rowSpacing
        let y = topBasedY + (metrics.cellHeight - metrics.totalItemHeight) / 2 - (layoutMode == .vertical ? verticalOffset : 0)
        return CGRect(x: x, y: y, width: metrics.cellWidth, height: metrics.totalItemHeight)
    }

    private func visibleFrame(_ frame: CGRect) -> CGRect {
        layoutMode == .paged ? frame.offsetBy(dx: horizontalOffset, dy: 0) : frame
    }

    private func pageOffset(for page: Int, metrics: Metrics) -> CGFloat {
        -CGFloat(page) * metrics.pageStride
    }

    private func clampHorizontalOffset(_ value: CGFloat, metrics: Metrics) -> CGFloat {
        let minOffset = -CGFloat(max(pageCount - 1, 0)) * metrics.pageStride
        return min(0, max(minOffset, value))
    }

    private func layoutSublayers(of layer: CALayer, metrics: Metrics) {
        layer.transform = CATransform3DIdentity
        let iconX = (metrics.cellWidth - iconSize) / 2
        let iconY = metrics.labelHeight + metrics.labelTopSpacing
        let iconFrame = CGRect(x: iconX, y: iconY, width: iconSize, height: iconSize)
        if let iconLayer = layer.sublayers?.first(where: { $0.name == "icon" }) {
            iconLayer.transform = CATransform3DIdentity
            iconLayer.frame = iconFrame
        }
        if let textLayer = layer.sublayers?.first(where: { $0.name == "label" }) as? CATextLayer {
            textLayer.isHidden = !showLabels
            textLayer.frame = CGRect(x: 4, y: 0, width: metrics.cellWidth - 8, height: metrics.labelHeight)
        }
        if let warningLayer = layer.sublayers?.first(where: { $0.name == "missingWarning" }) as? CATextLayer {
            let side = max(14, iconSize * 0.22)
            warningLayer.frame = CGRect(x: iconFrame.maxX - side - iconSize * 0.05,
                                        y: iconFrame.maxY - side - iconSize * 0.05,
                                        width: side,
                                        height: side)
            warningLayer.cornerRadius = side / 2
        }
        if let index = appLayers.firstIndex(of: layer) {
            applyScale(at: index, animated: false)
        }
    }

    private func setIcon(for layer: CALayer, app: AppInfo) {
        let path = app.url.path
        let scale = backingScale
        let side = iconSize
        let expectedKey = iconCacheKey(for: path, scale: scale)
        layer.setValue(expectedKey, forKey: "iconCacheKey")
        if let cached = cachedIcon(for: path) {
            layer.contents = cached
            return
        }
        layer.contents = nil
        DispatchQueue.global(qos: .userInitiated).async { [weak self, weak layer] in
            guard let self, let layer else { return }
            guard layer.value(forKey: "iconCacheKey") as? String == expectedKey else { return }
            let icon: NSImage
            if FileManager.default.fileExists(atPath: path) {
                icon = IconStore.shared.icon(forPath: path)
            } else {
                icon = MissingAppPlaceholder.defaultIcon
            }
            guard let cgImage = self.renderIcon(icon, key: expectedKey, side: side, scale: scale) else { return }
            DispatchQueue.main.async {
                guard layer.value(forKey: "iconCacheKey") as? String == expectedKey else { return }
                CATransaction.begin()
                CATransaction.setDisableActions(true)
                layer.contents = cgImage
                CATransaction.commit()
            }
        }
    }

    private func cachedIcon(for path: String) -> CGImage? {
        let key = iconCacheKey(for: path, scale: backingScale)
        iconCacheLock.lock()
        defer { iconCacheLock.unlock() }
        return iconCache[key]
    }

    private func iconCacheKey(for path: String, scale: CGFloat) -> String {
        "\(path)#\(Int(iconSize.rounded()))#\(Int((scale * 100).rounded()))"
    }

    private func renderIcon(_ icon: NSImage, key: String, side: CGFloat, scale: CGFloat) -> CGImage? {
        iconCacheLock.lock()
        if let cached = iconCache[key] {
            iconCacheLock.unlock()
            return cached
        }
        iconCacheLock.unlock()

        let pixelSide = max(16, Int((side * scale).rounded()))
        let image = NSImage(size: NSSize(width: pixelSide, height: pixelSide))
        image.lockFocus()
        NSGraphicsContext.current?.imageInterpolation = .high
        icon.draw(in: NSRect(x: 0, y: 0, width: pixelSide, height: pixelSide))
        image.unlockFocus()
        guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }

        iconCacheLock.lock()
        iconCache[key] = cgImage
        iconCacheLock.unlock()
        return cgImage
    }

    private func updateLabelFonts() {
        for layer in appLayers {
            if let text = layer.sublayers?.first(where: { $0.name == "label" }) as? CATextLayer {
                text.fontSize = labelFontSize
                text.font = NSFont.systemFont(ofSize: labelFontSize, weight: labelFontWeight)
            }
        }
    }

    private func updateLabelVisibility() {
        for layer in appLayers {
            if let text = layer.sublayers?.first(where: { $0.name == "label" }) as? CATextLayer {
                text.isHidden = !showLabels
            }
        }
    }

    private func updateLabelColors() {
        let resolvedColor = currentLabelColor().cgColor
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        for layer in appLayers {
            if let text = layer.sublayers?.first(where: { $0.name == "label" }) as? CATextLayer {
                text.foregroundColor = resolvedColor
            }
        }
        CATransaction.commit()
    }

    private func currentLabelColor() -> NSColor {
        let match = effectiveAppearance.bestMatch(from: [.darkAqua, .aqua])
        return match == .darkAqua ? .white : .black
    }

    override func viewDidChangeEffectiveAppearance() {
        super.viewDidChangeEffectiveAppearance()
        updateLabelColors()
    }

    override func mouseMoved(with event: NSEvent) {
        guard hoverMagnificationEnabled, !isDraggingItem else {
            updateHoverIndex(nil)
            return
        }
        let point = convert(event.locationInWindow, from: nil)
        updateHoverIndex(itemIndex(at: point))
    }

    override func mouseExited(with event: NSEvent) {
        updateHoverIndex(nil)
    }

    override func mouseDown(with event: NSEvent) {
        window?.makeFirstResponder(self)
        let point = convert(event.locationInWindow, from: nil)
        guard let index = itemIndex(at: point) else { return }
        pressedIndex = index
        dragStartPoint = point
        applyScale(at: index, animated: true)
    }

    override func mouseDragged(with event: NSEvent) {
        let point = convert(event.locationInWindow, from: nil)
        if !isDraggingItem, let pressedIndex {
            guard !isLayoutLocked else { return }
            let distance = hypot(point.x - dragStartPoint.x, point.y - dragStartPoint.y)
            if distance > 10, apps.indices.contains(pressedIndex) {
                startDragging(at: pressedIndex, point: point)
            }
        }
        if isDraggingItem {
            updateDragging(at: point)
        }
    }

    override func mouseUp(with event: NSEvent) {
        let point = convert(event.locationInWindow, from: nil)
        if isDraggingItem {
            finishDragging(at: point)
            return
        }
        if let index = pressedIndex {
            pressedIndex = nil
            applyScale(at: index, animated: true)
            if itemIndex(at: point) == index, apps.indices.contains(index) {
                let app = apps[index]
                if FileManager.default.fileExists(atPath: app.url.path) {
                    onOpenApp?(app)
                } else {
                    NSSound.beep()
                }
            }
        }
    }

    override func scrollWheel(with event: NSEvent) {
        if layoutMode == .paged {
            handlePagedScroll(event)
        } else {
            handleVerticalScroll(event)
        }
    }

    override func keyDown(with event: NSEvent) {
        if event.keyCode == 53 {
            onClose?()
            return
        }
        guard !apps.isEmpty else {
            super.keyDown(with: event)
            return
        }
        if selectedIndex == nil { updateSelection(0, animated: true) }
        guard let selectedIndex else { return }

        switch event.keyCode {
        case 36:
            let app = apps[selectedIndex]
            if FileManager.default.fileExists(atPath: app.url.path) {
                onOpenApp?(app)
            } else {
                NSSound.beep()
            }
        case 123:
            updateSelection(max(0, selectedIndex - 1), animated: true)
        case 124:
            updateSelection(min(apps.count - 1, selectedIndex + 1), animated: true)
        case 125:
            updateSelection(min(apps.count - 1, selectedIndex + makeMetrics().columns), animated: true)
        case 126:
            updateSelection(max(0, selectedIndex - makeMetrics().columns), animated: true)
        default:
            super.keyDown(with: event)
        }
    }

    private func handlePagedScroll(_ event: NSEvent) {
        let deltaX = event.scrollingDeltaX
        let deltaY = event.scrollingDeltaY
        let dominant = scaledPageDelta(deltaX: deltaX, deltaY: deltaY)

        if !event.hasPreciseScrollingDeltas {
            if dominant != 0 {
                handleWheelPaging(with: dominant)
            }
            return
        }

        let phase = event.phase
        let momentumPhase = event.momentumPhase
        let phaseLessScroll = phase.isEmpty && momentumPhase.isEmpty
        let ended = phase.contains(.ended)
            || phase.contains(.cancelled)
            || momentumPhase.contains(.ended)
            || momentumPhase.contains(.cancelled)

        if phase.contains(.began) {
            beginPageScroll()
        }

        if (phase.contains(.changed) || phaseLessScroll), dominant != 0 {
            if !(isPageScrollAnimating && !isPageScrollDragging) {
                if !isPageScrollDragging { beginPageScroll() }
                updatePageScroll(by: dominant)

                if phaseLessScroll {
                    schedulePageScrollSnap(velocity: dominant)
                }
            }
        }

        if ended {
            finishPageScroll(velocity: dominant)
        }
    }

    private func schedulePageScrollSnap(velocity: CGFloat) {
        pageScrollSnapWorkItem?.cancel()
        let workItem = DispatchWorkItem { [weak self] in
            self?.finishPageScroll(velocity: velocity)
        }
        pageScrollSnapWorkItem = workItem
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.12, execute: workItem)
    }

    private func scaledPageDelta(deltaX: CGFloat, deltaY: CGFloat) -> CGFloat {
        let rawDelta = abs(deltaX) > abs(deltaY) ? deltaX : -deltaY
        let baseline = max(AppStore.defaultScrollSensitivity, 0.0001)
        let sensitivityScale = CGFloat(max(scrollSensitivity, 0.0001) / baseline)
        return rawDelta * sensitivityScale
    }

    private func handleWheelPaging(with scaledDelta: CGFloat) {
        let direction = scaledDelta > 0 ? 1 : -1
        let effectiveDirection = reverseWheelPagingDirection ? -direction : direction
        if wheelLastDirection != direction {
            wheelAccumulatedDelta = 0
        }
        wheelLastDirection = direction
        wheelAccumulatedDelta += abs(scaledDelta)

        let threshold: CGFloat = 2.0
        guard wheelAccumulatedDelta >= threshold else { return }

        let now = Date()
        if let last = wheelLastFlipAt, now.timeIntervalSince(last) < wheelFlipCooldown {
            return
        }

        let targetPage = effectiveDirection > 0 ? currentPage - 1 : currentPage + 1
        wheelLastFlipAt = now
        wheelAccumulatedDelta = 0
        if targetPage < 0 || targetPage >= pageCount {
            animatePageBoundaryNudge(toward: targetPage)
            return
        }
        navigateToPage(targetPage, animated: true)
    }

    private func beginPageScroll() {
        pageScrollSnapWorkItem?.cancel()
        isPageScrollAnimating = false
        isPageScrollDragging = true
        pageScrollStartOffset = horizontalOffset
        pageScrollAccumulatedDelta = 0
    }

    private func updatePageScroll(by delta: CGFloat) {
        isPageScrollAnimating = false
        pageScrollAccumulatedDelta += delta
        let metrics = makeMetrics()
        let minOffset = pageOffset(for: max(pageCount - 1, 0), metrics: metrics)
        let maxOffset: CGFloat = 0
        var newOffset = pageScrollStartOffset + pageScrollAccumulatedDelta

        if newOffset > maxOffset {
            newOffset = maxOffset + rubberBand(newOffset - maxOffset, limit: bounds.width * 0.2)
        } else if newOffset < minOffset {
            newOffset = minOffset + rubberBand(newOffset - minOffset, limit: bounds.width * 0.2)
        }

        horizontalOffset = newOffset
        applyHorizontalOffset()
    }

    private func finishPageScroll(velocity: CGFloat) {
        pageScrollSnapWorkItem?.cancel()
        guard isPageScrollDragging else {
            if !isPageScrollAnimating {
                snapToNearestPage(animated: true)
            }
            return
        }
        isPageScrollDragging = false

        let metrics = makeMetrics()
        let pageStride = max(metrics.pageStride, 1)
        let threshold = pageStride * 0.15
        let velocityThreshold: CGFloat = 30
        var targetPage = Int(round(-horizontalOffset / pageStride))

        if pageScrollAccumulatedDelta < -threshold || velocity < -velocityThreshold {
            targetPage = max(targetPage, currentPage + 1)
        } else if pageScrollAccumulatedDelta > threshold || velocity > velocityThreshold {
            targetPage = min(targetPage, currentPage - 1)
        } else {
            targetPage = currentPage
        }

        pageScrollAccumulatedDelta = 0
        navigateToPage(targetPage, animated: true)
    }

    private func snapToNearestPage(animated: Bool) {
        let metrics = makeMetrics()
        let pageStride = max(metrics.pageStride, 1)
        let nearestPage = Int(round(-horizontalOffset / pageStride))
        navigateToPage(nearestPage, animated: animated)
    }

    private func handleVerticalScroll(_ event: NSEvent) {
        let metrics = makeMetrics()
        let raw = event.scrollingDeltaY
        let baseline = max(AppStore.defaultScrollSensitivity, 0.0001)
        let sensitivityScale = CGFloat(max(scrollSensitivity, 0.0001) / baseline)
        var delta = (event.hasPreciseScrollingDeltas ? raw : -raw) * sensitivityScale
        if reverseWheelPagingDirection {
            delta = -delta
        }
        verticalOffset = clampVerticalOffset(verticalOffset - delta, metrics: metrics)
        updateLayout(animated: false)
    }

    private func navigateToPage(_ page: Int, animated: Bool) {
        let target = min(max(0, page), max(pageCount - 1, 0))
        let metrics = makeMetrics()
        let resolvedOffset = clampHorizontalOffset(pageOffset(for: target, metrics: metrics), metrics: metrics)
        if animated, page != target, layoutMode == .paged, abs(horizontalOffset - resolvedOffset) <= 0.5 {
            animatePageBoundaryNudge(toward: page)
            return
        }
        currentPage = target
        targetHorizontalOffset = resolvedOffset
        wheelAccumulatedDelta = 0
        wheelLastDirection = 0
        let needsAnimation = animated && animationsEnabled && abs(horizontalOffset - targetHorizontalOffset) > 0.5
        if needsAnimation {
            setupDisplayLinkIfNeeded()
            isPageScrollAnimating = true
        } else {
            isPageScrollAnimating = false
            horizontalOffset = targetHorizontalOffset
        }
        applyHorizontalOffset()
        notifyPageStateChanged()
    }

    private func animatePageBoundaryNudge(toward requestedPage: Int) {
        guard layoutMode == .paged, pageCount > 0, !isPageScrollDragging else { return }
        let metrics = makeMetrics()
        let page = min(max(0, currentPage), max(pageCount - 1, 0))
        let baseOffset = clampHorizontalOffset(pageOffset(for: page, metrics: metrics), metrics: metrics)
        let direction: CGFloat = requestedPage < 0 ? 1 : -1
        let rawNudge = min(max(bounds.width * 0.08, 18), 44)
        horizontalOffset = baseOffset + direction * rawNudge
        targetHorizontalOffset = baseOffset
        currentPage = page
        isPageScrollAnimating = animationsEnabled

        if animationsEnabled {
            setupDisplayLinkIfNeeded()
        } else {
            horizontalOffset = targetHorizontalOffset
            isPageScrollAnimating = false
        }

        applyHorizontalOffset()
        notifyPageStateChanged()
    }

    private func notifyPageStateChanged() {
        guard bounds.width > 0, bounds.height > 0 else { return }
        let count = pageCount
        let page = min(max(0, currentPage), max(count - 1, 0))
        guard lastReportedPage != page || lastReportedPageCount != count else { return }
        lastReportedPage = page
        lastReportedPageCount = count
        onPageStateChanged?(page, count)
    }

    private func notifyVerticalScrollOffsetChanged() {
        let offset = layoutMode == .vertical ? max(0, -verticalOffset) : 0
        guard lastReportedVerticalOffset.map({ abs($0 - offset) > 0.5 }) ?? true else { return }
        lastReportedVerticalOffset = offset
        onVerticalScrollOffsetChanged?(offset)
    }

    private func updatePageScrollAnimation() {
        let metrics = makeMetrics()
        targetHorizontalOffset = clampHorizontalOffset(pageOffset(for: currentPage, metrics: metrics), metrics: metrics)
        if !animationsEnabled {
            horizontalOffset = targetHorizontalOffset
            isPageScrollAnimating = false
        } else {
            let diff = targetHorizontalOffset - horizontalOffset
            if abs(diff) > 0.5 {
                horizontalOffset += diff * 0.18
            } else {
                horizontalOffset = targetHorizontalOffset
                isPageScrollAnimating = false
            }
        }
        applyHorizontalOffset()

        if !isPageScrollAnimating, pendingDragUpdateAfterPageAnimation {
            pendingDragUpdateAfterPageAnimation = false
            if isDraggingItem {
                DispatchQueue.main.async { [weak self] in
                    guard let self, self.isDraggingItem else { return }
                    self.updateDragging(at: self.dragCurrentPoint)
                }
            }
        }
    }

    private func applyHorizontalOffset() {
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        CATransaction.setAnimationDuration(0)
        contentLayer.transform = layoutMode == .paged ? CATransform3DMakeTranslation(horizontalOffset, 0, 0) : CATransform3DIdentity
        CATransaction.commit()
        updateVisibleItemFrames()
    }

    private func updateVisibleItemFrames() {
        guard layoutMode == .paged else { return }
        for (index, layer) in appLayers.enumerated() where itemFrames.indices.contains(index) {
            itemFrames[index] = visibleFrame(layer.frame)
        }
    }

    private func clampVerticalOffset(_ value: CGFloat, metrics: Metrics) -> CGFloat {
        let minOffset = min(0, bounds.height - metrics.contentHeight)
        return min(0, max(minOffset, value))
    }

    private func rubberBand(_ offset: CGFloat, limit: CGFloat) -> CGFloat {
        let factor: CGFloat = 0.5
        let absOffset = abs(offset)
        let scaled = (factor * absOffset * limit) / (absOffset + limit)
        return offset >= 0 ? scaled : -scaled
    }

    private func itemIndex(at point: CGPoint) -> Int? {
        for (index, frame) in itemFrames.enumerated() where frame.contains(point) {
            return index
        }
        return nil
    }

    func contextMenuItemIndex(at point: CGPoint) -> Int? {
        itemIndex(at: point)
    }

    private func gridIndex(at point: CGPoint) -> Int? {
        guard bounds.width > 0, bounds.height > 0 else { return nil }
        let metrics = makeMetrics()
        if layoutMode == .paged {
            guard !isPageScrollAnimating else { return nil }
            let page = min(max(0, currentPage), max(pageCount - 1, 0))
            let localX = point.x - contentInsets.left
            let localY = bounds.height - point.y - contentInsets.top
            guard localX >= 0, localY >= 0 else { return nil }
            let col = Int(localX / (metrics.cellWidth + columnSpacing))
            let row = Int(localY / (metrics.cellHeight + rowSpacing))
            guard col >= 0, col < metrics.columns, row >= 0, row < metrics.rows else { return nil }
            return min(apps.count, page * metrics.itemsPerPage + row * metrics.columns + col)
        }

        let localX = point.x - contentInsets.left
        let localY = bounds.height - point.y - verticalOffset - contentInsets.top
        guard localX >= 0, localY >= 0 else { return nil }
        let col = Int(localX / (metrics.cellWidth + columnSpacing))
        let row = Int(localY / (metrics.cellHeight + rowSpacing))
        guard col >= 0, col < metrics.columns, row >= 0 else { return nil }
        return min(apps.count, row * metrics.columns + col)
    }

    private func startDragging(at index: Int, point: CGPoint) {
        guard apps.indices.contains(index) else { return }
        isDraggingItem = true
        draggingIndex = index
        draggingApp = apps[index]
        currentHoverIndex = nil
        pressedIndex = nil
        appLayers[index].opacity = 0
        draggingLayer = makeDraggingLayer(for: apps[index], at: point)
        if let draggingLayer { layer?.addSublayer(draggingLayer) }
    }

    private func makeDraggingLayer(for app: AppInfo, at point: CGPoint) -> CALayer {
        let container = CALayer()
        container.frame = CGRect(x: point.x - iconSize / 2, y: point.y - iconSize / 2, width: iconSize, height: iconSize)
        container.transform = CATransform3DMakeScale(1.08, 1.08, 1)
        container.shadowColor = NSColor.black.cgColor
        container.shadowOpacity = 0.18
        container.shadowRadius = 10
        container.shadowOffset = CGSize(width: 0, height: -3)
        let iconLayer = CALayer()
        iconLayer.contentsGravity = .resizeAspect
        iconLayer.contentsScale = backingScale
        iconLayer.frame = container.bounds
        if let cached = cachedIcon(for: app.url.path) {
            iconLayer.contents = cached
        }
        container.addSublayer(iconLayer)
        setIcon(for: iconLayer, app: app)
        return container
    }

    private func updateDragging(at point: CGPoint) {
        dragCurrentPoint = point
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        draggingLayer?.frame = CGRect(x: point.x - iconSize / 2, y: point.y - iconSize / 2, width: iconSize, height: iconSize)
        CATransaction.commit()

        if point.x < dragOutInset || point.y < dragOutInset || point.x > bounds.width - dragOutInset || point.y > bounds.height - dragOutInset {
            if let app = draggingApp {
                cancelDragging(restoreSource: false)
                onDragAppOut?(app)
            }
            return
        }

        if layoutMode == .paged {
            let edgeDirection: Int?
            if point.x < pageFlipEdgeWidth {
                edgeDirection = -1
            } else if point.x > bounds.width - pageFlipEdgeWidth {
                edgeDirection = 1
            } else {
                edgeDirection = nil
            }

            if let edgeDirection {
                if !edgeDragRequiresReentry {
                    startEdgeFlipTimer(direction: edgeDirection)
                }
            } else {
                edgeDragRequiresReentry = false
                cancelEdgeFlipTimer()
            }
            guard !isPageScrollAnimating else { return }
        }

        let hoverIndex = gridIndex(at: point)
        updateReorderPreview(targetIndex: hoverIndex == draggingIndex ? nil : hoverIndex)
    }

    private func startEdgeFlipTimer(direction: Int) {
        guard layoutMode == .paged, !isPageScrollAnimating else { return }
        let targetPage = currentPage + direction
        guard targetPage >= 0, targetPage < pageCount else {
            cancelEdgeFlipTimer()
            return
        }
        if edgeDragDirection == direction, edgeDragTimer != nil { return }
        cancelEdgeFlipTimer()
        edgeDragDirection = direction
        let timer = Timer(timeInterval: pageFlipDelay, repeats: false) { [weak self] _ in
            guard let self else { return }
            self.edgeDragTimer = nil
            self.edgeDragDirection = nil
            guard self.isDraggingItem else { return }
            let nextPage = self.currentPage + direction
            guard nextPage >= 0, nextPage < self.pageCount else { return }
            self.resetReorderPreview(animated: false)
            self.currentHoverIndex = nil
            self.edgeDragRequiresReentry = true
            self.pendingDragUpdateAfterPageAnimation = true
            self.navigateToPage(nextPage, animated: true)
            if !self.isPageScrollAnimating {
                self.pendingDragUpdateAfterPageAnimation = false
                DispatchQueue.main.async { [weak self] in
                    guard let self, self.isDraggingItem else { return }
                    self.updateDragging(at: self.dragCurrentPoint)
                }
            }
        }
        RunLoop.main.add(timer, forMode: .common)
        edgeDragTimer = timer
    }

    private func cancelEdgeFlipTimer() {
        edgeDragTimer?.invalidate()
        edgeDragTimer = nil
        edgeDragDirection = nil
    }

    private func finishDragging(at point: CGPoint) {
        guard let source = draggingIndex else {
            cancelDragging()
            return
        }
        finishPageAnimationImmediatelyIfNeeded()
        let target = currentHoverIndex ?? gridIndex(at: point) ?? source
        let clampedTarget = min(max(0, target), apps.count)
        let shouldReorder = source != clampedTarget
        if shouldReorder {
            finishDraggingAfterReorder()
            onReorderApps?(source, clampedTarget)
        } else {
            cancelDragging(restoreSource: true, animated: true)
        }
    }

    private func finishDraggingAfterReorder() {
        cancelEdgeFlipTimer()
        edgeDragRequiresReentry = false
        pendingDragUpdateAfterPageAnimation = false
        draggingLayer?.removeFromSuperlayer()
        draggingLayer = nil
        isDraggingItem = false
        currentHoverIndex = nil

        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            self.draggingIndex = nil
            self.draggingApp = nil
        }
    }

    private func cancelDragging(restoreSource: Bool = true, animated: Bool = true) {
        cancelEdgeFlipTimer()
        edgeDragRequiresReentry = false
        pendingDragUpdateAfterPageAnimation = false
        resetReorderPreview(animated: animated)
        if restoreSource, let index = draggingIndex, appLayers.indices.contains(index) {
            appLayers[index].opacity = 1
        }
        draggingLayer?.removeFromSuperlayer()
        draggingLayer = nil
        draggingIndex = nil
        draggingApp = nil
        isDraggingItem = false
        currentHoverIndex = nil
    }

    private func finishPageAnimationImmediatelyIfNeeded() {
        guard layoutMode == .paged, isPageScrollAnimating else { return }
        pageScrollSnapWorkItem?.cancel()
        pendingDragUpdateAfterPageAnimation = false
        isPageScrollAnimating = false
        horizontalOffset = targetHorizontalOffset
        applyHorizontalOffset()
    }

    private func updateReorderPreview(targetIndex: Int?) {
        guard let source = draggingIndex, apps.indices.contains(source) else { return }
        let clampedTarget = targetIndex.map { min(max(0, $0), apps.count) }
        guard currentHoverIndex != clampedTarget else { return }
        currentHoverIndex = clampedTarget

        let metrics = makeMetrics()
        if layoutMode == .paged {
            updatePagedReorderPreview(source: source, target: clampedTarget, metrics: metrics)
        } else {
            updateVerticalReorderPreview(source: source, target: clampedTarget, metrics: metrics)
        }
    }

    private func updatePagedReorderPreview(source: Int, target: Int?, metrics: Metrics) {
        let pageStart = currentPage * metrics.itemsPerPage
        let pageEnd = min(pageStart + metrics.itemsPerPage, apps.count)
        guard pageStart < pageEnd else { return }
        let sourceInCurrentPage = source >= pageStart && source < pageEnd
        let hoverLocalIndex: Int? = {
            guard let target, target >= pageStart, target <= pageEnd else { return nil }
            return min(max(0, target - pageStart), max(0, pageEnd - pageStart))
        }()

        CATransaction.begin()
        CATransaction.setAnimationDuration(animationsEnabled ? 0.28 : 0)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(controlPoints: 0.25, 1.0, 0.35, 1.0))

        for index in apps.indices {
            guard appLayers.indices.contains(index) else { continue }
            let layer = appLayers[index]
            if index == source {
                layer.opacity = 0
                continue
            }

            guard index >= pageStart, index < pageEnd else {
                layer.opacity = 1
                continue
            }

            let localIndex = index - pageStart
            var visualIndex = index
            if let hoverLocalIndex {
                if sourceInCurrentPage {
                    let sourceLocalIndex = source - pageStart
                    if sourceLocalIndex < hoverLocalIndex,
                       localIndex > sourceLocalIndex,
                       localIndex <= hoverLocalIndex {
                        visualIndex = index - 1
                    } else if sourceLocalIndex > hoverLocalIndex,
                              localIndex >= hoverLocalIndex,
                              localIndex < sourceLocalIndex {
                        visualIndex = index + 1
                    }
                } else if localIndex >= hoverLocalIndex {
                    visualIndex = index + 1
                }
            }

            let frame = frameForGridSlot(at: visualIndex, metrics: metrics)
            layer.transform = CATransform3DIdentity
            layer.frame = frame
            if itemFrames.indices.contains(index) {
                itemFrames[index] = visibleFrame(frame)
            }
            layoutSublayers(of: layer, metrics: metrics)
            layer.opacity = 1
        }

        CATransaction.commit()
    }

    private func updateVerticalReorderPreview(source: Int, target: Int?, metrics: Metrics) {
        let visualOrder = visualOrderForDrag(source: source, target: target)

        CATransaction.begin()
        CATransaction.setAnimationDuration(animationsEnabled ? 0.28 : 0)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(controlPoints: 0.25, 1.0, 0.35, 1.0))

        for index in apps.indices {
            guard appLayers.indices.contains(index) else { continue }
            let layer = appLayers[index]
            if index == source {
                layer.opacity = 0
                continue
            }
            guard let visualIndex = visualOrder.firstIndex(of: index) else { continue }
            let frame = frameForGridSlot(at: visualIndex, metrics: metrics)
            layer.transform = CATransform3DIdentity
            layer.frame = frame
            if itemFrames.indices.contains(index) {
                itemFrames[index] = visibleFrame(frame)
            }
            layoutSublayers(of: layer, metrics: metrics)
            layer.opacity = 1
        }

        CATransaction.commit()
    }

    private func visualOrderForDrag(source: Int, target: Int?) -> [Int] {
        var order = Array(apps.indices)
        guard order.indices.contains(source) else { return order }
        let moving = order.remove(at: source)
        if let target {
            order.insert(moving, at: min(max(0, target), order.count))
        } else {
            order.insert(moving, at: source)
        }
        return order
    }

    private func resetReorderPreview(animated: Bool) {
        guard !appLayers.isEmpty else { return }
        let metrics = makeMetrics()

        CATransaction.begin()
        CATransaction.setDisableActions(!animated)
        CATransaction.setAnimationDuration(animated && animationsEnabled ? 0.2 : 0)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeOut))

        itemFrames = Array(repeating: .zero, count: apps.count)
        for index in apps.indices {
            guard appLayers.indices.contains(index) else { continue }
            let frame = frameForGridSlot(at: index, metrics: metrics)
            itemFrames[index] = visibleFrame(frame)
            appLayers[index].transform = CATransform3DIdentity
            appLayers[index].frame = frame
            layoutSublayers(of: appLayers[index], metrics: metrics)
        }

        CATransaction.commit()
    }

    private func ensureSelectionVisible() {
        guard let selectedIndex else { return }
        if layoutMode == .paged {
            let metrics = makeMetrics()
            let page = selectedIndex / metrics.itemsPerPage
            if page != currentPage {
                navigateToPage(page, animated: true)
            }
            return
        }
        guard itemFrames.indices.contains(selectedIndex) else { return }
        let frame = itemFrames[selectedIndex]
        let metrics = makeMetrics()
        if frame.minY < contentInsets.bottom {
            verticalOffset -= contentInsets.bottom - frame.minY
        } else if frame.maxY > bounds.height - contentInsets.top {
            verticalOffset += frame.maxY - (bounds.height - contentInsets.top)
        }
        verticalOffset = clampVerticalOffset(verticalOffset, metrics: metrics)
        updateLayout(animated: true)
    }

    private func updateHoverIndex(_ index: Int?) {
        guard hoveredIndex != index else { return }
        let old = hoveredIndex
        hoveredIndex = index
        if let old { applyScale(at: old, animated: true) }
        if let index { applyScale(at: index, animated: true) }
    }

    private func applyScale(at index: Int, animated: Bool) {
        guard appLayers.indices.contains(index) else { return }
        let layer = appLayers[index]
        var iconScale: CGFloat = 1
        if selectedIndex == index {
            iconScale = 1.16
        } else if hoverMagnificationEnabled && hoveredIndex == index {
            iconScale = hoverMagnificationScale
        }
        let pressScale: CGFloat = (activePressEffectEnabled && pressedIndex == index) ? activePressScale : 1
        CATransaction.begin()
        CATransaction.setDisableActions(!animated)
        CATransaction.setAnimationDuration(animated ? 0.12 : 0)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeOut))
        layer.transform = CATransform3DMakeScale(pressScale, pressScale, 1)
        if let icon = layer.sublayers?.first(where: { $0.name == "icon" }) {
            icon.transform = CATransform3DMakeScale(iconScale, iconScale, 1)
        }
        CATransaction.commit()
    }
}

```

## /LaunchNext/CAFolderGridViewRepresentable.swift

```swift path="/LaunchNext/CAFolderGridViewRepresentable.swift" 
import AppKit
import SwiftUI

struct CAFolderGridViewRepresentable: NSViewRepresentable {
    @ObservedObject var appStore: AppStore
    @Binding var folder: FolderInfo
    @Binding var currentPage: Int
    @Binding var pageCount: Int
    @Binding var verticalScrollOffset: CGFloat
    var iconSize: CGFloat
    var verticalHeaderHeight: CGFloat
    var onClose: () -> Void
    var onLaunchApp: (AppInfo) -> Void

    func makeNSView(context: Context) -> CAFolderGridView {
        let view = CAFolderGridView(frame: .zero)
        configure(view)
        wireCallbacks(view)
        view.apps = folder.apps
        return view
    }

    func updateNSView(_ nsView: CAFolderGridView, context: Context) {
        configure(nsView)
        if nsView.apps != folder.apps {
            nsView.apps = folder.apps
        }
        if appStore.folderLayoutMode == .paged, nsView.displayedPage != currentPage {
            nsView.setDisplayedPage(currentPage, animated: appStore.enableAnimations)
        }
    }

    private func configure(_ view: CAFolderGridView) {
        view.layoutMode = appStore.folderLayoutMode
        view.iconSize = iconSize
        view.labelFontSize = CGFloat(appStore.iconLabelFontSize)
        view.labelFontWeight = nsFontWeight(for: appStore.iconLabelFontWeight)
        view.showLabels = appStore.showLabels
        view.hoverMagnificationEnabled = appStore.enableHoverMagnification
        view.hoverMagnificationScale = CGFloat(appStore.hoverMagnificationScale)
        view.activePressEffectEnabled = appStore.enableActivePressEffect
        view.activePressScale = CGFloat(appStore.activePressScale)
        view.animationsEnabled = appStore.enableAnimations
        view.animationDuration = appStore.animationDuration
        view.isLayoutLocked = appStore.isLayoutLocked
        view.scrollSensitivity = appStore.scrollSensitivity
        view.reverseWheelPagingDirection = appStore.reverseWheelPagingDirection
        view.verticalHeaderHeight = verticalHeaderHeight
        view.showInFinderMenuTitle = appStore.localized(.contextMenuShowInFinder)
        view.copyAppPathMenuTitle = appStore.localized(.contextMenuCopyAppPath)
        view.hideAppMenuTitle = appStore.localized(.hiddenAppsAddButton)
        view.uninstallWithToolMenuTitle = appStore.localized(.contextMenuUninstallWithConfiguredTool)
        view.canUseConfiguredUninstallTool = appStore.uninstallToolAppURL != nil
    }

    private func wireCallbacks(_ view: CAFolderGridView) {
        let currentPageBinding = $currentPage
        let pageCountBinding = $pageCount
        let verticalScrollOffsetBinding = $verticalScrollOffset
        view.onOpenApp = { app in
            DispatchQueue.main.async {
                onLaunchApp(app)
            }
        }
        view.onClose = {
            DispatchQueue.main.async {
                onClose()
            }
        }
        view.onPageStateChanged = { page, count in
            DispatchQueue.main.async {
                if currentPageBinding.wrappedValue != page {
                    currentPageBinding.wrappedValue = page
                }
                if pageCountBinding.wrappedValue != count {
                    pageCountBinding.wrappedValue = count
                }
            }
        }
        view.onVerticalScrollOffsetChanged = { offset in
            DispatchQueue.main.async {
                if abs(verticalScrollOffsetBinding.wrappedValue - offset) > 0.5 {
                    verticalScrollOffsetBinding.wrappedValue = offset
                }
            }
        }
        view.onReorderApps = { from, to in
            DispatchQueue.main.async {
                _ = appStore.reorderAppInFolder(folderID: folder.id, from: from, to: to)
            }
        }
        view.onDragAppOut = { app in
            DispatchQueue.main.async {
                appStore.handoffDraggingApp = app
                appStore.handoffDragScreenLocation = NSEvent.mouseLocation
                appStore.removeAppFromFolder(app, folder: folder)
                withAnimation(LNAnimations.springFast) {
                    onClose()
                }
            }
        }
        view.onShowAppInFinder = { app in
            DispatchQueue.main.async {
                if !appStore.showAppInFinder(app) { NSSound.beep() }
            }
        }
        view.onCopyAppPath = { app in
            DispatchQueue.main.async {
                if !appStore.copyAppPath(app) { NSSound.beep() }
            }
        }
        view.onHideApp = { app in
            DispatchQueue.main.async {
                _ = appStore.hideApp(app)
            }
        }
        view.onUninstallWithTool = { app in
            DispatchQueue.main.async {
                if !appStore.openConfiguredUninstallTool(for: app) { NSSound.beep() }
            }
        }
    }

    private func nsFontWeight(for option: AppStore.IconLabelFontWeightOption) -> NSFont.Weight {
        switch option {
        case .light: return .light
        case .regular: return .regular
        case .medium: return .medium
        case .semibold: return .semibold
        case .bold: return .bold
        }
    }
}

```

## /LaunchNext/CAGridView+Input.swift

```swift path="/LaunchNext/CAGridView+Input.swift" 
import AppKit
import QuartzCore

extension CAGridView {
    // MARK: - Input Handling

    override var acceptsFirstResponder: Bool { true }

    override func becomeFirstResponder() -> Bool {
        // print("🎯 [CAGrid] becomeFirstResponder")
        return true
    }

    override func resignFirstResponder() -> Bool {
        // print("🎯 [CAGrid] resignFirstResponder")
        return true
    }

    // 确保视图接受第一次鼠标点击就能响应
    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
        return true
    }

    // 确保视图可以接收鼠标事件
    override func hitTest(_ point: NSPoint) -> NSView? {
        let result = frame.contains(point) ? self : nil
        return result
    }

    override func updateTrackingAreas() {
        super.updateTrackingAreas()
        if let existing = hoverTrackingArea {
            removeTrackingArea(existing)
        }
        let options: NSTrackingArea.Options = [.mouseMoved, .mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect]
        let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
        addTrackingArea(area)
        hoverTrackingArea = area
    }

    override func mouseMoved(with event: NSEvent) {
        guard hoverMagnificationEnabled else {
            clearHover()
            return
        }
        guard !isDraggingItem && !isPageDragging && !isDragging else {
            clearHover()
            return
        }
        let location = convert(event.locationInWindow, from: nil)
        if let (item, _) = itemAt(location), case .empty = item {
            updateHoverIndex(nil)
        } else if let (_, index) = itemAt(location) {
            updateHoverIndex(index)
        } else {
            updateHoverIndex(nil)
        }
    }

    override func mouseExited(with event: NSEvent) {
        clearHover()
    }

    override func viewDidChangeEffectiveAppearance() {
        super.viewDidChangeEffectiveAppearance()
        updateLabelColors()
        updateFolderGlassColors()
    }

    override func scrollWheel(with event: NSEvent) {
        // 当本地 monitor 存在时,避免双重处理
        if scrollEventMonitor != nil {
            return
        }
        guard isScrollEnabled else { return }
        handleScrollWheel(with: event)
    }

    func handleScrollWheel(with event: NSEvent) {
        // 优先使用水平滑动,如果没有则用垂直滑动(反向)
        let deltaX = event.scrollingDeltaX
        let deltaY = event.scrollingDeltaY
        let delta = abs(deltaX) > abs(deltaY) ? deltaX : -deltaY
        let baseline = max(AppStore.defaultScrollSensitivity, 0.0001)
        let sensitivityScale = CGFloat(max(scrollSensitivity, 0.0001) / baseline)
        let scaledDelta = delta * sensitivityScale
        let isPrecise = event.hasPreciseScrollingDeltas

        if !isPrecise {
            /*
            // 旧版滚轮跟手 + 定时器 snap 逻辑(保留注释,便于后续对比)
            wheelSnapTimer?.invalidate()

            // 累积滚动量
            wheelAccumulatedDelta += scaledDelta * 8  // 放大系数,让跟手效果更明显

            // 计算临时偏移(带橡皮筋效果)
            let pageStride = bounds.width + pageSpacing
            let baseOffset = -CGFloat(currentPage) * pageStride
            var newOffset = baseOffset + wheelAccumulatedDelta

            // 橡皮筋效果:边界阻力
            let minOffset = -CGFloat(pageCount - 1) * pageStride
            let maxOffset: CGFloat = 0
            if newOffset > maxOffset {
                let overscroll = newOffset - maxOffset
                newOffset = maxOffset + rubberBand(overscroll, limit: bounds.width * 0.15)
            } else if newOffset < minOffset {
                let overscroll = newOffset - minOffset
                newOffset = minOffset + rubberBand(overscroll, limit: bounds.width * 0.15)
            }

            // 更新显示
            scrollOffset = newOffset
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            pageContainerLayer.transform = CATransform3DMakeTranslation(scrollOffset, 0, 0)
            CATransaction.commit()

            // 设置定时器,停止滚动后决定翻页或弹回
            wheelSnapTimer = Timer.scheduledTimer(withTimeInterval: wheelSnapDelay, repeats: false) { [weak self] _ in
                guard let self = self else { return }

                let threshold = self.bounds.width * 0.15  // 15% 触发翻页
                var targetPage = self.currentPage

                if self.wheelAccumulatedDelta < -threshold {
                    targetPage = self.currentPage + 1
                } else if self.wheelAccumulatedDelta > threshold {
                    targetPage = self.currentPage - 1
                }

                self.wheelAccumulatedDelta = 0
                self.navigateToPage(targetPage, animated: true)
            }
            */

            // 只优化普通滚轮,不改精准设备(触控板 / Magic Mouse)路径
            handleWheelPaging(with: scaledDelta)
            return
        }

        // 触控板滑动
        switch event.phase {
        case .began:
            isDragging = true
            isScrollAnimating = false
            dragStartOffset = scrollOffset
            accumulatedDelta = 0
            scrollVelocity = 0

        case .changed:
            accumulatedDelta += scaledDelta

            // 计算新的偏移量
            var newOffset = dragStartOffset + accumulatedDelta

            // 橡皮筋效果:在边界处添加阻力
            let pageStride = bounds.width + pageSpacing
            let minOffset = -CGFloat(pageCount - 1) * pageStride
            let maxOffset: CGFloat = 0

            if newOffset > maxOffset {
                // 超出左边界
                let overscroll = newOffset - maxOffset
                newOffset = maxOffset + rubberBand(overscroll, limit: bounds.width * 0.2)
            } else if newOffset < minOffset {
                // 超出右边界
                let overscroll = newOffset - minOffset
                newOffset = minOffset + rubberBand(overscroll, limit: bounds.width * 0.2)
            }

            scrollOffset = newOffset

            // 性能优化:使用 CATransaction 批量更新
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            CATransaction.setAnimationDuration(0)
            pageContainerLayer.transform = CATransform3DMakeTranslation(scrollOffset, 0, 0)
            CATransaction.commit()

        case .ended, .cancelled:
            isDragging = false

            // 根据滑动距离和速度确定目标页面
            let velocity = (abs(deltaX) > abs(deltaY) ? deltaX : -deltaY) * sensitivityScale
            let threshold = (bounds.width + pageSpacing) * 0.15  // 15% 即可触发翻页
            let velocityThreshold: CGFloat = 30
            var targetPage = currentPage

            // 根据累计滑动方向决定翻页
            if accumulatedDelta < -threshold || velocity < -velocityThreshold {
                targetPage = currentPage + 1
            } else if accumulatedDelta > threshold || velocity > velocityThreshold {
                targetPage = currentPage - 1
            }

            navigateToPage(targetPage)

        default:
            break
        }
    }

    private func handleWheelPaging(with scaledDelta: CGFloat) {
        guard scaledDelta != 0 else { return }

        let direction = scaledDelta > 0 ? 1 : -1
        let effectiveDirection = reverseWheelPagingDirection ? -direction : direction
        if wheelLastDirection != direction {
            wheelAccumulatedDelta = 0
        }
        wheelLastDirection = direction
        wheelAccumulatedDelta += abs(scaledDelta)

        // 固定阈值,灵敏度变化已反映在 scaledDelta
        let threshold: CGFloat = 2.0
        guard wheelAccumulatedDelta >= threshold else { return }

        let now = Date()
        if let last = wheelLastFlipAt, now.timeIntervalSince(last) < wheelFlipCooldown {
            return
        }

        // Keep existing CA direction semantics by default; optional override flips wheel-only paging.
        let targetPage = effectiveDirection > 0 ? currentPage - 1 : currentPage + 1
        wheelLastFlipAt = now
        wheelAccumulatedDelta = 0
        navigateToPage(targetPage, animated: true)
    }

    func rubberBand(_ offset: CGFloat, limit: CGFloat) -> CGFloat {
        let factor: CGFloat = 0.5
        let absOffset = abs(offset)
        let scaled = (factor * absOffset * limit) / (absOffset + limit)
        return offset >= 0 ? scaled : -scaled
    }

    override func mouseDown(with event: NSEvent) {
        guard !externalAppDragSessionActive else { return }
        // 确保成为第一响应者,这样后续的滚轮事件才能被接收
        window?.makeFirstResponder(self)

        let location = convert(event.locationInWindow, from: nil)
        // print("🖱️ [CAGrid] mouseDown at \(location)")

        if let (item, index) = itemAt(location) {
            // print("🖱️ [CAGrid] Hit item: \(item.name) at index \(index)")
            if event.clickCount == 1 {
                // 添加点击效果动画
                pressedIndex = index
                animatePress(at: index, pressed: true)
                dragStartPoint = location

                // 启动长按计时器(用于开始拖拽)
                // 注意:必须添加到 .common 模式,否则在鼠标追踪期间不会触发
                longPressTimer?.invalidate()
                let timer = Timer(timeInterval: longPressDuration, repeats: false) { [weak self] _ in
                    self?.startDragging(item: item, index: index, at: location)
                }
                RunLoop.main.add(timer, forMode: .common)
                longPressTimer = timer
            }
        } else {
            // 点击空白区域 - 开始页面拖拽模式
            // print("🖱️ [CAGrid] Hit empty area, starting page drag")
            isPageDragging = true
            pageDragStartX = location.x
            pageDragStartOffset = scrollOffset
            dragStartPoint = location
        }
    }

    override func mouseDragged(with event: NSEvent) {
        if externalAppDragSessionActive { return }
        let location = convert(event.locationInWindow, from: nil)

        // 页面拖拽模式
        if isPageDragging {
            let deltaX = location.x - pageDragStartX
            var newOffset = pageDragStartOffset + deltaX

            // 橡皮筋效果 - 在边界处添加阻力
            let pageStride = bounds.width + pageSpacing
            let minOffset = -CGFloat(pageCount - 1) * pageStride
            let maxOffset: CGFloat = 0

            if newOffset > maxOffset {
                let overscroll = newOffset - maxOffset
                newOffset = maxOffset + rubberBand(overscroll, limit: bounds.width * 0.3)
            } else if newOffset < minOffset {
                let overscroll = newOffset - minOffset
                newOffset = minOffset + rubberBand(overscroll, limit: bounds.width * 0.3)
            }

            scrollOffset = newOffset

            CATransaction.begin()
            CATransaction.setDisableActions(true)
            pageContainerLayer.transform = CATransform3DMakeTranslation(scrollOffset, 0, 0)
            CATransaction.commit()
            return
        }

        // Check if moved enough distance to start dragging
        if !isDraggingItem, let idx = pressedIndex {
            if isLayoutLocked { return }
            let distance = hypot(location.x - dragStartPoint.x, location.y - dragStartPoint.y)
            if distance > 10 {
                // 取消长按计时器,立即开始拖拽
                longPressTimer?.invalidate()
                longPressTimer = nil
                if let item = items[safe: idx] {
                    startDragging(item: item, index: idx, at: location)
                }
            }
        }

        // 更新拖拽位置
        if isDraggingItem {
            let dragDelta = CGPoint(x: location.x - dragCurrentPoint.x,
                                    y: location.y - dragCurrentPoint.y)
            if let app = externalDockDragCandidate(),
               shouldStartExternalDockDrag(localPoint: location,
                                           windowPoint: event.locationInWindow,
                                           dragDelta: dragDelta) {
                startExternalDockDrag(for: app, event: event, at: location)
                return
            }
            updateDragging(at: location)
        }
    }

    override func mouseUp(with event: NSEvent) {
        if externalAppDragSessionActive { return }
        let location = convert(event.locationInWindow, from: nil)

        // 取消长按计时器
        longPressTimer?.invalidate()
        longPressTimer = nil

        // 结束页面拖拽
        if isPageDragging {
            isPageDragging = false

            let totalDrag = location.x - pageDragStartX
            let threshold = (bounds.width + pageSpacing) * 0.15  // 15% 即可触发翻页

            var targetPage = currentPage
            if totalDrag < -threshold {
                // 向左拖 -> 下一页
                targetPage = min(currentPage + 1, pageCount - 1)
            } else if totalDrag > threshold {
                // 向右拖 -> 上一页
                targetPage = max(currentPage - 1, 0)
            }

            // 如果没有实际拖动(只是点击),则关闭窗口
            if abs(totalDrag) < 5 {
                onEmptyAreaClicked?()
                return
            }

            navigateToPage(targetPage, animated: true)
            return
        }

        if isDraggingItem {
            // 结束拖拽
            endDragging(at: location)
        } else if let idx = pressedIndex {
            // 恢复点击效果
            pressedIndex = nil
            animatePress(at: idx, pressed: false)

            // 检查是否在同一个 item 上释放
            if let (item, index) = itemAt(location), index == idx {
                if isBatchSelectionMode {
                    if case .app(let app) = item {
                        toggleBatchSelection(forAppPath: app.url.path)
                    }
                } else {
                    // 延迟一点点再触发,让动画效果更明显
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
                        self?.onItemClicked?(item, index)
                    }
                }
            }
        }
    }


    func animatePress(at index: Int, pressed: Bool) {
        _ = pressed
        let pageIndex = index / itemsPerPage
        let localIndex = index % itemsPerPage

        guard pageIndex < iconLayers.count, localIndex < iconLayers[pageIndex].count else { return }

        applyScaleForIndex(index, animated: true)
    }

    // MARK: - Drag and Drop

    func startDragging(item: LaunchpadItem, index: Int, at point: CGPoint) {
        guard !isLayoutLocked else { return }
        if isBatchSelectionMode {
            guard case .app(let app) = item else { return }
            let dragPath = app.url.path
            let orderedBatch = orderedBatchDragPaths(leadingAppPath: dragPath)
            guard !orderedBatch.isEmpty else { return }
            batchDraggingAppPathsOrdered = orderedBatch
            batchHiddenCompanionIndices = orderedBatch
                .compactMap { globalIndex(forAppPath: $0) }
                .filter { $0 != index }
        } else {
            // Allow dragging apps and folders in normal mode.
            switch item {
            case .app, .folder:
                break
            case .empty, .missingApp:
                return
            }
            batchDraggingAppPathsOrdered.removeAll()
            batchHiddenCompanionIndices.removeAll()
        }

        isDraggingItem = true
        draggingIndex = index
        draggingItem = item
        dragCurrentPoint = point

        // 恢复按压效果
        if let idx = pressedIndex {
            animatePress(at: idx, pressed: false)
            pressedIndex = nil
        }

        // 隐藏原图标
        let pageIndex = index / itemsPerPage
        let localIndex = index % itemsPerPage
        if pageIndex < iconLayers.count, localIndex < iconLayers[pageIndex].count {
            iconLayers[pageIndex][localIndex].opacity = 0
        }
        if !batchHiddenCompanionIndices.isEmpty {
            for companionIndex in batchHiddenCompanionIndices {
                setOpacity(0, forGlobalIndex: companionIndex)
            }
        }

        // 创建拖拽图层
        createDraggingLayer(for: item, at: point)

        if isBatchDragging {
            pendingHoverIndex = gridPositionAt(point)
            applyIconPositionUpdate()
        }

        // print("🎯 [CAGrid] Started dragging: \(item.name) at index \(index)")
    }

    func createDraggingLayer(for item: LaunchpadItem, at point: CGPoint) {
        let actualIconSize = iconSize
        let scale = NSScreen.main?.backingScaleFactor ?? 2.0
        
        // Container for dragging (holds glass + icon for folders)
        let container = CALayer()
        container.frame = CGRect(x: point.x - actualIconSize / 2, y: point.y - actualIconSize / 2,
                            width: actualIconSize, height: actualIconSize)
        container.transform = CATransform3DMakeScale(1.1, 1.1, 1.0)
        container.zPosition = 1000

        // For folders, add glass background
        if case .folder = item {
            let glassSize = actualIconSize * 0.8
            let glassOffset = (actualIconSize - glassSize) / 2
            let glassLayer = CALayer()
            glassLayer.frame = CGRect(x: glassOffset, y: glassOffset, width: glassSize, height: glassSize)
            glassLayer.backgroundColor = NSColor.white.withAlphaComponent(0.08).cgColor
            glassLayer.borderColor = NSColor.white.withAlphaComponent(0.2).cgColor
            glassLayer.borderWidth = 0.5
            glassLayer.cornerRadius = glassSize * 0.25
            glassLayer.shadowColor = NSColor.black.cgColor
            glassLayer.shadowOffset = CGSize(width: 0, height: -1)
            glassLayer.shadowRadius = 3
            glassLayer.shadowOpacity = 0.15
            container.addSublayer(glassLayer)
        }

        // Icon layer
        let iconLayer = CALayer()
        iconLayer.frame = CGRect(x: 0, y: 0, width: actualIconSize, height: actualIconSize)
        iconLayer.contentsScale = scale
        iconLayer.contentsGravity = .resizeAspect
        iconLayer.shadowOpacity = 0

        // Set icon content with high resolution
        if case .app(let app) = item {
            let icon = IconStore.shared.icon(forPath: app.url.path)
            let renderSize = NSSize(width: actualIconSize * scale, height: actualIconSize * scale)
            let renderedImage = NSImage(size: renderSize)
            renderedImage.lockFocus()
            icon.draw(in: NSRect(origin: .zero, size: renderSize))
            renderedImage.unlockFocus()
            if let cgImage = renderedImage.cgImage(forProposedRect: nil, context: nil, hints: nil) {
                iconLayer.contents = cgImage
            }
        } else if case .folder(let folder) = item {
            let icon = folder.icon(of: actualIconSize, scale: folderPreviewScale)
            let renderSize = NSSize(width: actualIconSize * scale, height: actualIconSize * scale)
            let renderedImage = NSImage(size: renderSize)
            renderedImage.lockFocus()
            icon.draw(in: NSRect(origin: .zero, size: renderSize))
            renderedImage.unlockFocus()
            if let cgImage = renderedImage.cgImage(forProposedRect: nil, context: nil, hints: nil) {
                iconLayer.contents = cgImage
            }
        }

        container.addSublayer(iconLayer)

        if case .app = item, batchDraggingAppPathsOrdered.count > 1 {
            addBatchDragCountBadge(to: container, count: batchDraggingAppPathsOrdered.count)
        }

        containerLayer.addSublayer(container)
        draggingLayer = container
    }

    func externalDockDragCandidate() -> AppInfo? {
        guard !isBatchDragging else { return nil }
        guard case .app(let app) = draggingItem else { return nil }
        let path = app.url.path
        guard !path.isEmpty else { return nil }
        guard app.url.pathExtension.lowercased() == "app" else { return nil }
        guard FileManager.default.fileExists(atPath: path) else { return nil }
        return app
    }

    func shouldStartExternalDockDrag(localPoint point: CGPoint, windowPoint: CGPoint, dragDelta: CGPoint) -> Bool {
        guard let contentView = window?.contentView else { return false }
        guard dockDragEnabled else { return false }

        switch dockDragSide {
        case .disabled:
            return false
        case .bottom:
            let movingTowardDock = dragDelta.y < -1.5
            let nearBottomEdge = windowPoint.y <= contentView.bounds.minY + externalAppDragTriggerDistance
            let isInsideHorizontalRange =
                windowPoint.x >= contentView.bounds.minX - externalAppDragOutset &&
                windowPoint.x <= contentView.bounds.maxX + externalAppDragOutset
            return movingTowardDock && nearBottomEdge && isInsideHorizontalRange

        case .left:
            let movingTowardDock = dragDelta.x < -1.5
            let nearLeftEdge = windowPoint.x <= contentView.bounds.minX + externalAppDragTriggerDistance
            let isInsideVerticalRange =
                windowPoint.y >= contentView.bounds.minY - externalAppDragOutset &&
                windowPoint.y <= contentView.bounds.maxY + externalAppDragOutset
            return movingTowardDock && nearLeftEdge && isInsideVerticalRange

        case .right:
            let movingTowardDock = dragDelta.x > 1.5
            let nearRightEdge = windowPoint.x >= contentView.bounds.maxX - externalAppDragTriggerDistance
            let isInsideVerticalRange =
                windowPoint.y >= contentView.bounds.minY - externalAppDragOutset &&
                windowPoint.y <= contentView.bounds.maxY + externalAppDragOutset
            return movingTowardDock && nearRightEdge && isInsideVerticalRange
        }
    }

    func startExternalDockDrag(for app: AppInfo, event: NSEvent, at point: CGPoint) {
        guard !externalAppDragSessionActive else { return }

        clearDropTargetHighlight()
        cancelEdgeDragTimer()

        let writer = app.url as NSURL
        let draggingItem = NSDraggingItem(pasteboardWriter: writer)

        let dragImage = renderedExternalDockDragPreview(for: app)
        let frame = CGRect(x: point.x - iconSize / 2,
                           y: point.y - iconSize / 2,
                           width: iconSize,
                           height: iconSize)
        draggingItem.setDraggingFrame(frame, contents: dragImage)

        cancelDragging()

        externalAppDragSessionActive = true
        AppDelegate.shared?.beginExternalSystemDragSession()

        let session = beginDraggingSession(with: [draggingItem], event: event, source: self)
        session.draggingFormation = .none
        session.animatesToStartingPositionsOnCancelOrFail = true
    }

    func renderedExternalDockDragPreview(for app: AppInfo) -> NSImage {
        let icon = IconStore.shared.icon(forPath: app.url.path)
        let scale = window?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2
        let renderSize = NSSize(width: iconSize * scale, height: iconSize * scale)
        let renderedImage = NSImage(size: renderSize)
        renderedImage.lockFocus()
        icon.draw(in: NSRect(origin: .zero, size: renderSize),
                  from: .zero,
                  operation: .copy,
                  fraction: 1.0)
        renderedImage.unlockFocus()
        renderedImage.size = NSSize(width: iconSize, height: iconSize)
        return renderedImage
    }

    func addBatchDragCountBadge(to container: CALayer, count: Int) {
        let badgeSize: CGFloat = 22
        let badge = CALayer()
        badge.name = "batchDragCountBadge"
        badge.frame = CGRect(x: container.bounds.width - badgeSize * 0.9,
                             y: container.bounds.height - badgeSize * 0.95,
                             width: badgeSize,
                             height: badgeSize)
        badge.cornerRadius = badgeSize * 0.5
        badge.backgroundColor = NSColor.systemBlue.cgColor
        badge.borderColor = NSColor.white.withAlphaComponent(0.85).cgColor
        badge.borderWidth = 1
        badge.zPosition = 40

        let text = CATextLayer()
        text.string = "\(count)"
        text.alignmentMode = .center
        text.font = NSFont.systemFont(ofSize: 11, weight: .bold)
        text.fontSize = 11
        text.foregroundColor = NSColor.white.cgColor
        text.contentsScale = NSScreen.main?.backingScaleFactor ?? 2
        text.frame = CGRect(x: 0, y: 4, width: badgeSize, height: badgeSize - 6)
        badge.addSublayer(text)
        container.addSublayer(badge)
    }

    func updateDragging(at point: CGPoint) {
        dragCurrentPoint = point

        // Update dragging layer position
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        let actualIconSize = iconSize
        draggingLayer?.frame = CGRect(x: point.x - actualIconSize / 2, y: point.y - actualIconSize / 2,
                                      width: actualIconSize, height: actualIconSize)
        CATransaction.commit()

        // Check edge drag for page flip
        checkEdgeDrag(at: point)

        let dragPage = draggingIndex.map { $0 / itemsPerPage }
        let isCrossPage = dragPage != nil && dragPage != currentPage

        if isBatchDragging {
            if let hoverIndex = gridPositionAt(point), hoverIndex != draggingIndex {
                clearDropTargetHighlight()
                updateIconPositionsForDrag(hoverIndex: hoverIndex)
            } else {
                clearDropTargetHighlight()
                updateIconPositionsForDrag(hoverIndex: nil)
            }
            return
        }

        if let hoverIndex = gridPositionAt(point), hoverIndex != draggingIndex {
            if hoverIndex < items.count {
                let targetItem = items[hoverIndex]
                let inCenterArea = isPointInFolderDropZone(point, targetIndex: hoverIndex)

                switch targetItem {
                case .folder:
                    if case .app = draggingItem, inCenterArea {
                        // Hovering over folder center - highlight for move into folder
                        highlightDropTarget(at: hoverIndex)
                        updateIconPositionsForDrag(hoverIndex: isCrossPage ? hoverIndex : nil)
                    } else {
                        clearDropTargetHighlight()
                        updateIconPositionsForDrag(hoverIndex: hoverIndex)
                    }
                case .app:
                    if case .app = draggingItem, inCenterArea {
                        // App over app center - create folder
                        highlightDropTarget(at: hoverIndex)
                        updateIconPositionsForDrag(hoverIndex: isCrossPage ? hoverIndex : nil)
                    } else {
                        clearDropTargetHighlight()
                        updateIconPositionsForDrag(hoverIndex: hoverIndex)
                    }
                case .missingApp, .empty:
                    clearDropTargetHighlight()
                    updateIconPositionsForDrag(hoverIndex: hoverIndex)
                }
            } else {
                clearDropTargetHighlight()
                updateIconPositionsForDrag(hoverIndex: hoverIndex)
            }
        } else {
            clearDropTargetHighlight()
            updateIconPositionsForDrag(hoverIndex: nil)
        }
    }
    
    func updateIconPositionsForDrag(hoverIndex: Int?) {
        guard draggingIndex != nil else { return }
        
        // Skip if same as pending or current
        if hoverIndex == pendingHoverIndex { return }
        
        // Store pending hover index
        pendingHoverIndex = hoverIndex
        
        // Cancel previous timer
        hoverUpdateTimer?.invalidate()
        
        // Schedule delayed update to prevent jittering during fast movement
        hoverUpdateTimer = Timer.scheduledTimer(withTimeInterval: hoverUpdateDelay, repeats: false) { [weak self] _ in
            self?.applyIconPositionUpdate()
        }
    }
    
    func applyIconPositionUpdate() {
        guard let dragIndex = draggingIndex else { return }
        
        let hoverIndex = pendingHoverIndex
        
        // Batch mode always recomputes compaction so selected gaps are closed immediately.
        if !isBatchDragging, hoverIndex == currentHoverIndex { return }
        currentHoverIndex = hoverIndex
        
        // Get current page icons only
        let pageIndex = currentPage
        guard pageIndex < iconLayers.count else { return }
        let pageLayers = iconLayers[pageIndex]
        let pageStart = pageIndex * itemsPerPage

        ensureOriginalPositionsForCurrentPage(pageLayers: pageLayers, pageStart: pageStart)
        
        if isBatchDragging {
            applyBatchCompactedPositions(pageLayers: pageLayers, pageStart: pageStart, dragIndex: dragIndex)
            return
        }
        
        // Calculate positions with item shifted - smooth spring-like animation
        CATransaction.begin()
        CATransaction.setAnimationDuration(0.45)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(controlPoints: 0.25, 1.0, 0.35, 1.0))
        
        for (localIndex, layer) in pageLayers.enumerated() {
            let globalIndex = pageStart + localIndex
            
            // Skip the dragging item's original layer - hide it completely
            if globalIndex == dragIndex {
                layer.opacity = 0
                continue
            }
            
            guard let originalPos = originalIconPositions[globalIndex] else { continue }
            
            var targetPos = originalPos
            
            if let hover = hoverIndex {
                let hoverLocalIndex = hover - pageStart
                let dragLocalIndex = dragIndex - pageStart
                let dragInThisPage = dragIndex >= pageStart && dragIndex < pageStart + itemsPerPage
                
                // Only affect items on current page
                if hover >= pageStart && hover < pageStart + itemsPerPage {
                    
                    if dragInThisPage {
                        if dragLocalIndex < hoverLocalIndex {
                            // Dragging forward: items between drag and hover shift left
                            if localIndex > dragLocalIndex && localIndex <= hoverLocalIndex {
                                if let prevPos = originalIconPositions[pageStart + localIndex - 1] {
                                    targetPos = prevPos
                                }
                            }
                        } else if dragLocalIndex > hoverLocalIndex {
                            // Dragging backward: items between hover and drag shift right
                            if localIndex >= hoverLocalIndex && localIndex < dragLocalIndex {
                                if let nextPos = originalIconPositions[pageStart + localIndex + 1] {
                                    targetPos = nextPos
                                }
                            }
                        }
                    } else {
                        // Dragging from another page: create a gap on the hover page by shifting items to the right.
                        if localIndex >= hoverLocalIndex {
                            let targetGlobalIndex = pageStart + localIndex + 1
                            targetPos = gridCenterForGlobalIndex(targetGlobalIndex)
                        }
                    }
                }
            }
            
            layer.position = targetPos
        }
        
        CATransaction.commit()
    }

    func applyBatchCompactedPositions(pageLayers: [CALayer], pageStart: Int, dragIndex: Int) {
        let pageEnd = pageStart + pageLayers.count
        let removedIndices = Set(batchHiddenCompanionIndices + [dragIndex]).filter { $0 >= pageStart && $0 < pageEnd }
        let removedLocals = Set(removedIndices.map { $0 - pageStart })

        var nonEmptyLocals: [Int] = []
        var emptyLocals: [Int] = []
        for localIndex in 0..<pageLayers.count {
            guard !removedLocals.contains(localIndex) else { continue }
            let globalIndex = pageStart + localIndex
            if globalIndex < items.count, case .empty = items[globalIndex] {
                emptyLocals.append(localIndex)
            } else {
                nonEmptyLocals.append(localIndex)
            }
        }
        let compactedLocals = nonEmptyLocals + emptyLocals

        CATransaction.begin()
        CATransaction.setAnimationDuration(0.25)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(controlPoints: 0.25, 0.9, 0.35, 1.0))

        for (targetLocalIndex, sourceLocalIndex) in compactedLocals.enumerated() {
            let layer = pageLayers[sourceLocalIndex]
            let targetGlobalIndex = pageStart + targetLocalIndex
            let targetPosition = originalIconPositions[targetGlobalIndex] ?? gridCenterForGlobalIndex(targetGlobalIndex)
            layer.position = targetPosition
            layer.opacity = 1
        }

        for removedLocalIndex in removedLocals {
            pageLayers[removedLocalIndex].opacity = 0
        }

        CATransaction.commit()
    }

    func gridCenterForGlobalIndex(_ globalIndex: Int) -> CGPoint {
        guard bounds.width > 0, bounds.height > 0 else { return .zero }

        let pageWidth = bounds.width
        let pageHeight = bounds.height
        let pageStride = pageWidth + pageSpacing
        let pageIndex = max(0, globalIndex / itemsPerPage)
        let localIndex = max(0, globalIndex % itemsPerPage)

        let availableWidth = max(0, pageWidth - contentInsets.left - contentInsets.right)
        let availableHeight = max(0, pageHeight - contentInsets.top - contentInsets.bottom)
        let totalColumnSpacing = columnSpacing * CGFloat(max(columns - 1, 0))
        let totalRowSpacing = rowSpacing * CGFloat(max(rows - 1, 0))
        let usableWidth = max(0, availableWidth - totalColumnSpacing)
        let usableHeight = max(0, availableHeight - totalRowSpacing)
        let cellWidth = usableWidth / CGFloat(max(columns, 1))
        let cellHeight = usableHeight / CGFloat(max(rows, 1))
        let strideX = cellWidth + columnSpacing

        let col = localIndex % columns
        let row = localIndex / columns

        let cellOriginX = contentInsets.left + CGFloat(col) * strideX
        let cellOriginY = pageHeight - contentInsets.top - CGFloat(row + 1) * cellHeight - CGFloat(row) * rowSpacing

        let actualIconSize = iconSize
        let labelHeight: CGFloat = labelFontSize + 8
        let labelTopSpacing: CGFloat = 6
        let totalHeight = actualIconSize + labelTopSpacing + labelHeight

        let containerX = CGFloat(pageIndex) * pageStride + cellOriginX
        let containerY = cellOriginY + (cellHeight - totalHeight) / 2
        return CGPoint(x: containerX + cellWidth / 2, y: containerY + totalHeight / 2)
    }
    
    func resetIconPositions() {
        // Cancel pending update timer
        hoverUpdateTimer?.invalidate()
        hoverUpdateTimer = nil
        pendingHoverIndex = nil
        
        guard !originalIconPositions.isEmpty else { 
            // Even if we never captured original positions, make sure the hidden drag source is restored
            if let dragIndex = draggingIndex {
                let pageIndex = dragIndex / itemsPerPage
                let localIndex = dragIndex % itemsPerPage
                if pageIndex < iconLayers.count, localIndex < iconLayers[pageIndex].count {
                    iconLayers[pageIndex][localIndex].opacity = 1.0
                }
            }
            currentHoverIndex = nil
            return 
        }
        
        CATransaction.begin()
        CATransaction.setAnimationDuration(0.3)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(controlPoints: 0.25, 0.1, 0.25, 1.0))
        
        for (pageIndex, pageLayers) in iconLayers.enumerated() {
            let pageStart = pageIndex * itemsPerPage
            for (localIndex, layer) in pageLayers.enumerated() {
                let globalIndex = pageStart + localIndex
                if let originalPos = originalIconPositions[globalIndex] {
                    layer.position = originalPos
                }
                layer.opacity = 1.0
            }
        }
        
        CATransaction.commit()
        
        originalIconPositions.removeAll()
        currentHoverIndex = nil
    }

    func updateHoverIndex(_ newIndex: Int?) {
        guard hoveredIndex != newIndex else { return }
        let old = hoveredIndex
        hoveredIndex = newIndex
        if let old = old {
            applyScaleForIndex(old, animated: true)
        }
        if let newIndex = newIndex {
            applyScaleForIndex(newIndex, animated: true)
        }
    }

    func clearHover() {
        updateHoverIndex(nil)
    }

    func updateLabelFonts() {
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        for pageLayers in iconLayers {
            for containerLayer in pageLayers {
                if let textLayer = containerLayer.sublayers?.first(where: { $0.name == "label" }) as? CATextLayer {
                    textLayer.font = NSFont.systemFont(ofSize: labelFontSize, weight: labelFontWeight)
                    textLayer.fontSize = labelFontSize
                }
            }
        }
        CATransaction.commit()
    }

    func updateLabelVisibility() {
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        for pageLayers in iconLayers {
            for containerLayer in pageLayers {
                if let textLayer = containerLayer.sublayers?.first(where: { $0.name == "label" }) as? CATextLayer {
                    textLayer.isHidden = !showLabels
                }
            }
        }
        CATransaction.commit()
        updateLayout()
    }

    func updateLabelColors() {
        let resolvedColor = currentLabelColor().cgColor
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        for pageLayers in iconLayers {
            for containerLayer in pageLayers {
                if let textLayer = containerLayer.sublayers?.first(where: { $0.name == "label" }) as? CATextLayer {
                    textLayer.foregroundColor = resolvedColor
                }
            }
        }
        CATransaction.commit()
    }

    func updateFolderGlassColors() {
        let colors = currentFolderGlassStyle()
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        for pageLayers in iconLayers {
            for containerLayer in pageLayers {
                if let glassLayer = containerLayer.sublayers?.first(where: { $0.name == "glass" }) {
                    glassLayer.backgroundColor = colors.background.cgColor
                    glassLayer.borderColor = colors.border.cgColor
                    glassLayer.shadowOffset = colors.shadowOffset
                    glassLayer.shadowRadius = colors.shadowRadius
                    glassLayer.shadowOpacity = colors.shadowOpacity
                }
            }
        }
        CATransaction.commit()
    }

    func currentLabelColor() -> NSColor {
        let match = effectiveAppearance.bestMatch(from: [.darkAqua, .aqua])
        return match == .darkAqua ? .white : .black
    }

    func isPointInFolderDropZone(_ point: CGPoint, targetIndex: Int) -> Bool {
        guard let center = iconCenter(for: targetIndex) else { return false }
        let size = iconSize * folderDropZoneScale
        let rect = CGRect(x: center.x - size / 2,
                          y: center.y - size / 2,
                          width: size,
                          height: size)
        return rect.contains(point)
    }

    func iconCenter(for index: Int) -> CGPoint? {
        guard bounds.width > 0, bounds.height > 0 else { return nil }
        let pageIndex = index / itemsPerPage
        let localIndex = index % itemsPerPage
        guard pageIndex >= 0 && pageIndex < pageCount else { return nil }

        let pageWidth = bounds.width
        let pageHeight = bounds.height
        let pageStride = pageWidth + pageSpacing

        let availableWidth = max(0, pageWidth - contentInsets.left - contentInsets.right)
        let availableHeight = max(0, pageHeight - contentInsets.top - contentInsets.bottom)
        let totalColumnSpacing = columnSpacing * CGFloat(max(columns - 1, 0))
        let totalRowSpacing = rowSpacing * CGFloat(max(rows - 1, 0))
        let usableWidth = max(0, availableWidth - totalColumnSpacing)
        let usableHeight = max(0, availableHeight - totalRowSpacing)
        let cellWidth = usableWidth / CGFloat(max(columns, 1))
        let cellHeight = usableHeight / CGFloat(max(rows, 1))
        let strideX = cellWidth + columnSpacing

        let col = localIndex % columns
        let row = localIndex / columns

        let cellOriginX = contentInsets.left + CGFloat(col) * strideX
        let cellOriginY = pageHeight - contentInsets.top - CGFloat(row + 1) * cellHeight - CGFloat(row) * rowSpacing

        let labelHeight: CGFloat = showLabels ? (labelFontSize + 8) : 0
        let labelTopSpacing: CGFloat = showLabels ? 6 : 0
        let totalHeight = iconSize + labelTopSpacing + labelHeight

        let containerX = CGFloat(pageIndex) * pageStride + cellOriginX
        let containerY = cellOriginY + (cellHeight - totalHeight) / 2

        let iconX = containerX + (cellWidth - iconSize) / 2
        let iconY = containerY + labelHeight + labelTopSpacing

        let centerX = iconX + iconSize / 2 + scrollOffset
        let centerY = iconY + iconSize / 2
        return CGPoint(x: centerX, y: centerY)
    }

    func currentFolderGlassStyle() -> (background: NSColor, border: NSColor, shadowOpacity: Float, shadowRadius: CGFloat, shadowOffset: CGSize) {
        let match = effectiveAppearance.bestMatch(from: [.darkAqua, .aqua])
        if match == .darkAqua {
            return (NSColor.white.withAlphaComponent(0.08),
                    NSColor.white.withAlphaComponent(0.2),
                    0.15,
                    3,
                    CGSize(width: 0, height: -1))
        }
        return (NSColor.white.withAlphaComponent(0.7),
                NSColor.white.withAlphaComponent(0.75),
                0.2,
                4,
                CGSize(width: 0, height: -1))
    }

    func applyScaleForIndex(_ index: Int, animated: Bool) {
        let pageIndex = index / itemsPerPage
        let localIndex = index % itemsPerPage
        guard pageIndex < iconLayers.count, localIndex < iconLayers[pageIndex].count else { return }

        let containerLayer = iconLayers[pageIndex][localIndex]

        let pressScale: CGFloat = (pressedIndex == index && activePressEffectEnabled) ? CGFloat(activePressScale) : 1.0

        let selectionScale: CGFloat = 1.2
        var iconScale: CGFloat = 1.0
        if dropTargetIndex == index {
            iconScale = 1.1
        } else if selectedIndex == index {
            iconScale = selectionScale
        } else if hoverMagnificationEnabled, hoveredIndex == index {
            iconScale = hoverMagnificationScale
        }

        CATransaction.begin()
        CATransaction.setAnimationDuration(animated ? 0.12 : 0)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeOut))
        containerLayer.transform = CATransform3DMakeScale(pressScale, pressScale, 1.0)

        if let iconLayer = containerLayer.sublayers?.first(where: { $0.name == "icon" }) {
            iconLayer.transform = CATransform3DMakeScale(iconScale, iconScale, 1.0)
        }
        if let glassLayer = containerLayer.sublayers?.first(where: { $0.name == "glass" }) {
            glassLayer.transform = CATransform3DMakeScale(iconScale, iconScale, 1.0)
        }
        CATransaction.commit()
    }

    func updateSelection(_ index: Int?, animated: Bool = true) {
        let clampedIndex: Int? = {
            guard let index else { return nil }
            guard index >= 0, index < items.count else { return nil }
            if case .empty = items[index] { return nil }
            return index
        }()

        guard selectedIndex != clampedIndex else { return }
        let old = selectedIndex
        selectedIndex = clampedIndex
        if let old = old { applyScaleForIndex(old, animated: animated) }
        if let newIndex = selectedIndex { applyScaleForIndex(newIndex, animated: animated) }
    }

    func updateExternalDragState(sourceIndex: Int?, hoverIndex: Int?) {
        // Avoid interfering with native CA drag.
        guard !isDraggingItem else { return }
        guard bounds.width > 0, bounds.height > 0 else { return }

        if let sourceIndex = sourceIndex {
            if !externalDragActive || draggingIndex != sourceIndex {
                externalDragActive = true
                draggingIndex = sourceIndex

                let pageIndex = sourceIndex / itemsPerPage
                let localIndex = sourceIndex % itemsPerPage
                if pageIndex < iconLayers.count, localIndex < iconLayers[pageIndex].count {
                    iconLayers[pageIndex][localIndex].opacity = 0
                }
            }
            updateIconPositionsForDrag(hoverIndex: hoverIndex)
        } else if externalDragActive {
            externalDragActive = false
            draggingIndex = nil
            resetIconPositions()
        }
    }

    func ensureOriginalPositionsForCurrentPage(pageLayers: [CALayer], pageStart: Int) {
        // If we already captured positions for this page, keep them.
        var hasAny = false
        for localIndex in 0..<pageLayers.count {
            let globalIndex = pageStart + localIndex
            if originalIconPositions[globalIndex] != nil {
                hasAny = true
                break
            }
        }
        guard !hasAny else { return }

        for (localIndex, layer) in pageLayers.enumerated() {
            let globalIndex = pageStart + localIndex
            originalIconPositions[globalIndex] = layer.position
        }
    }

    // MARK: - 边缘翻页检测
    func checkEdgeDrag(at point: CGPoint) {
        let leftEdge = point.x < edgeDragThreshold
        let rightEdge = point.x > bounds.width - edgeDragThreshold

        if leftEdge && currentPage > 0 {
            // 左边缘 - 翻到上一页
            startEdgeDragTimer(direction: -1)
        } else if rightEdge {
            // 右边缘 - 翻到下一页(可能创建新页)
            startEdgeDragTimer(direction: 1)
        } else {
            // 离开边缘区域 - 取消计时器
            cancelEdgeDragTimer()
        }
    }

    func startEdgeDragTimer(direction: Int) {
        // 如果已有相同方向的计时器,不重复创建
        if edgeDragTimer != nil { return }

        let timer = Timer(timeInterval: edgeDragDelay, repeats: false) { [weak self] _ in
            guard let self = self else { return }
            let targetPage = self.currentPage + direction

            // 检查是否需要创建新页面
            if direction > 0 && targetPage >= self.pageCount {
                // 通知创建新页面
                self.onRequestNewPage?()
            }

            self.navigateToPage(targetPage, animated: true)
            self.edgeDragTimer = nil

            // 翻页后继续检测
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
                guard let self = self, self.isDraggingItem else { return }
                self.checkEdgeDrag(at: self.dragCurrentPoint)
            }
        }
        RunLoop.main.add(timer, forMode: .common)
        edgeDragTimer = timer
    }

    func cancelEdgeDragTimer() {
        edgeDragTimer?.invalidate()
        edgeDragTimer = nil
    }

    func hardSnapToCurrentPage() {
        guard bounds.width > 0 else { return }
        resetScrollInteractionState()
        isScrollAnimating = false
        scrollAnimationStartTime = 0
        let expectedOffset = -CGFloat(currentPage) * (bounds.width + pageSpacing)
        scrollOffset = expectedOffset
        targetScrollOffset = expectedOffset
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        CATransaction.setAnimationDuration(0)
        pageContainerLayer.transform = CATransform3DMakeTranslation(scrollOffset, 0, 0)
        CATransaction.commit()
    }

    func resetScrollInteractionState() {
        isDragging = false
        accumulatedDelta = 0
        wheelAccumulatedDelta = 0
        wheelLastDirection = 0
        wheelLastFlipAt = nil
    }

    /// 计算点击位置对应的网格位置(即使是空白区域)
    func gridPositionAt(_ point: CGPoint) -> Int? {
        let pageWidth = bounds.width
        let pageHeight = bounds.height
        let adjustedX = point.x - scrollOffset

        // 计算点击的页面
        let pageStride = pageWidth + pageSpacing
        let pageIndex = Int(floor(adjustedX / pageStride))
        guard pageIndex >= 0 else { return nil }
        // 允许拖拽到最后一页之后(会创建新页)
        let effectivePageIndex = min(pageIndex, max(0, pageCount - 1))

        let availableWidth = max(0, pageWidth - contentInsets.left - contentInsets.right)
        let availableHeight = max(0, pageHeight - contentInsets.top - contentInsets.bottom)
        let totalColumnSpacing = columnSpacing * CGFloat(max(columns - 1, 0))
        let totalRowSpacing = rowSpacing * CGFloat(max(rows - 1, 0))
        let usableWidth = max(0, availableWidth - totalColumnSpacing)
        let usableHeight = max(0, availableHeight - totalRowSpacing)
        let cellWidth = usableWidth / CGFloat(max(columns, 1))
        let cellHeight = usableHeight / CGFloat(max(rows, 1))
        let strideX = cellWidth + columnSpacing
        let strideY = cellHeight + rowSpacing

        // 计算点击位置相对于当前页的坐标
        let pageX = adjustedX - CGFloat(effectivePageIndex) * pageStride
        guard pageX >= 0, pageX <= pageWidth else { return nil }
        let localX = pageX - contentInsets.left
        let localY = pageHeight - point.y - contentInsets.top

        // 钳制到有效范围
        let clampedX = max(0, min(localX, availableWidth - 1))
        let clampedY = max(0, min(localY, availableHeight - 1))

        let col = Int(clampedX / strideX)
        let row = Int(clampedY / strideY)

        let clampedCol = max(0, min(col, columns - 1))
        let clampedRow = max(0, min(row, rows - 1))

        let localIndex = clampedRow * columns + clampedCol
        let globalIndex = effectivePageIndex * itemsPerPage + localIndex

        return globalIndex
    }

    func highlightDropTarget(at index: Int) {
        // 清除之前的高亮
        if let oldTarget = dropTargetIndex, oldTarget != index {
            dropTargetIndex = nil
            applyScaleForIndex(oldTarget, animated: true)
        }

        dropTargetIndex = index
        applyScaleForIndex(index, animated: true)
    }

    func clearDropTargetHighlight() {
        if let target = dropTargetIndex {
            dropTargetIndex = nil
            applyScaleForIndex(target, animated: true)
        }
    }

    func setHighlight(at index: Int, highlighted _: Bool) {
        applyScaleForIndex(index, animated: true)
    }

    func endDragging(at point: CGPoint) {
        guard let dragIndex = draggingIndex, let dragItem = draggingItem else {
            cancelDragging()
            return
        }

        // Save current hover position before clearing
        let savedHoverIndex = currentHoverIndex

        // Clear highlights and reset
        clearDropTargetHighlight()
        cancelEdgeDragTimer()

        // Calculate target position
        let targetPosition = gridPositionAt(point)

        // Track if we're doing a reorder (so we don't reset positions unnecessarily)
        var didReorder = false

        if isBatchDragging {
            if let insertIndex = savedHoverIndex ?? targetPosition {
                let clampedIndex = max(0, min(insertIndex, items.count))
                let singleDragNoop = batchDraggingAppPathsOrdered.count == 1 && clampedIndex == dragIndex
                if !singleDragNoop {
                    onReorderAppBatch?(batchDraggingAppPathsOrdered, clampedIndex)
                    didReorder = true
                }
            }
        } else {
            // 检查是否拖到另一个item上
            if let (targetItem, targetIndex) = itemAt(point), targetIndex != dragIndex {
                // print("🎯 [CAGrid] Dropped on item: \(targetItem.name) at index \(targetIndex)")
                // 拖拽到另一个 item 上
                if case .app(let dragApp) = dragItem {
                    switch targetItem {
                    case .app(let targetApp):
                        // 两个应用 -> 创建文件夹
                        // print("📁 [CAGrid] Creating folder: \(dragApp.name) + \(targetApp.name)")
                        onCreateFolder?(dragApp, targetApp, targetIndex)
                        cancelDragging()
                        return
                    case .folder(let folder):
                        // 拖到文件夹 -> 移入文件夹
                        // print("📂 [CAGrid] Moving to folder: \(dragApp.name) -> \(folder.name)")
                        onMoveToFolder?(dragApp, folder)
                        cancelDragging()
                        return
                    case .empty, .missingApp:
                        // 空白格子或丢失的应用 -> 当作重排序处理
                        // print("🔄 [CAGrid] Dropped on empty/missing, reordering: \(dragIndex) -> \(targetIndex)")
                        onReorderItems?(dragIndex, targetIndex)
                        didReorder = true
                    }
                }
            }

            // Reorder to empty area - use saved hover position or calculated position
            if !didReorder, let insertIndex = savedHoverIndex ?? targetPosition, insertIndex != dragIndex {
                onReorderItems?(dragIndex, insertIndex)
                didReorder = true
            }
        }

        // If we did a reorder, data will update and rebuild layers
        // Delay clearing dragging state to avoid visual glitches during rebuild
        if didReorder {
            // Save drag index before clearing
            let savedDragIndex = draggingIndex
            
            // Remove dragging layer immediately
            draggingLayer?.removeFromSuperlayer()
            draggingLayer = nil
            
            // Clear dragging flags immediately
            isDraggingItem = false
            dropTargetIndex = nil
            
            // Cancel pending updates
            hoverUpdateTimer?.invalidate()
            hoverUpdateTimer = nil
            pendingHoverIndex = nil
            currentHoverIndex = nil
            
            // Delay clearing other state to let data update complete
            DispatchQueue.main.async { [weak self] in
                guard let self = self else { return }
                self.draggingIndex = nil
                self.draggingItem = nil
                self.originalIconPositions.removeAll()
                
                // Restore opacity of dragged item (if layers still exist)
                if let dragIndex = savedDragIndex {
                    let pageIndex = dragIndex / self.itemsPerPage
                    let localIndex = dragIndex % self.itemsPerPage
                    if pageIndex < self.iconLayers.count, localIndex < self.iconLayers[pageIndex].count {
                        self.iconLayers[pageIndex][localIndex].opacity = 1.0
                    }
                }
                self.restoreBatchHiddenCompanionLayers()
                if self.isBatchSelectionMode {
                    self.disableBatchSelectionMode()
                }
                self.forceSyncPageTransformIfNeeded()
            }
        } else {
            // No reorder happened, reset positions to original
            cancelDragging()
        }
        hardSnapToCurrentPage()
        logIfMismatch("endDragging")
    }

    func cancelDragging() {
        // Reset icon positions to original
        resetIconPositions()

        // Remove dragging layer
        draggingLayer?.removeFromSuperlayer()
        draggingLayer = nil

        isDraggingItem = false
        draggingIndex = nil
        draggingItem = nil
        dropTargetIndex = nil
        restoreBatchHiddenCompanionLayers()
        hardSnapToCurrentPage()
        logIfMismatch("cancelDragging")
    }

    func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
        context == .outsideApplication ? .copy : []
    }

    func ignoreModifierKeys(for session: NSDraggingSession) -> Bool {
        true
    }

    func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) {
        externalAppDragSessionActive = false
        AppDelegate.shared?.endExternalSystemDragSession()
    }

    func itemAt(_ point: CGPoint) -> (LaunchpadItem, Int)? {
        let pageWidth = bounds.width
        let pageHeight = bounds.height
        let adjustedX = point.x - scrollOffset

        // 计算点击的页面
        let pageStride = pageWidth + pageSpacing
        let pageIndex = Int(floor(adjustedX / pageStride))
        guard pageIndex >= 0 && pageIndex < pageCount else { return nil }

        let availableWidth = max(0, pageWidth - contentInsets.left - contentInsets.right)
        let availableHeight = max(0, pageHeight - contentInsets.top - contentInsets.bottom)
        let totalColumnSpacing = columnSpacing * CGFloat(max(columns - 1, 0))
        let totalRowSpacing = rowSpacing * CGFloat(max(rows - 1, 0))
        let usableWidth = max(0, availableWidth - totalColumnSpacing)
        let usableHeight = max(0, availableHeight - totalRowSpacing)
        let cellWidth = usableWidth / CGFloat(max(columns, 1))
        let cellHeight = usableHeight / CGFloat(max(rows, 1))
        let strideX = cellWidth + columnSpacing
        let strideY = cellHeight + rowSpacing

        // 计算点击位置相对于当前页的坐标
        let pageX = adjustedX - CGFloat(pageIndex) * pageStride
        guard pageX >= 0, pageX <= pageWidth else { return nil }
        let localX = pageX - contentInsets.left
        let localY = pageHeight - point.y - contentInsets.top

        guard localX >= 0, localY >= 0 else { return nil }
        guard localX < availableWidth, localY < availableHeight else { return nil }

        let col = Int(localX / strideX)
        let row = Int(localY / strideY)

        guard col >= 0, col < columns, row >= 0, row < rows else { return nil }

        let cellOriginX = CGFloat(col) * strideX
        let cellOriginY = CGFloat(row) * strideY
        let cellLocalX = localX - cellOriginX
        let cellLocalY = localY - cellOriginY

        guard cellLocalX >= 0, cellLocalX <= cellWidth else { return nil }
        guard cellLocalY >= 0, cellLocalY <= cellHeight else { return nil }

        let localIndex = row * columns + col
        let globalIndex = pageIndex * itemsPerPage + localIndex

        guard globalIndex < items.count else { return nil }

        // 检查是否点击在图标+标签区域内(不是单元格的空白部分)
        let actualIconSize = iconSize
        let labelHeight: CGFloat = showLabels ? (labelFontSize + 8) : 0
        let labelTopSpacing: CGFloat = showLabels ? 6 : 0
        let totalItemHeight = actualIconSize + labelTopSpacing + labelHeight

        // 图标+标签区域居中于单元格
        let itemStartX = (cellWidth - actualIconSize) / 2
        let itemEndX = itemStartX + actualIconSize
        let itemStartY = (cellHeight - totalItemHeight) / 2
        let itemEndY = itemStartY + totalItemHeight

        // 检查是否在图标+标签区域内
        guard cellLocalX >= itemStartX && cellLocalX <= itemEndX else { return nil }
        guard cellLocalY >= itemStartY && cellLocalY <= itemEndY else { return nil }

        return (items[globalIndex], globalIndex)
    }

}

```

## /LaunchNext/CAGridView+Layout.swift

```swift path="/LaunchNext/CAGridView+Layout.swift" 
import AppKit
import QuartzCore

extension CAGridView {
    // MARK: - Layer Management

    func rebuildLayers() {
        CATransaction.begin()
        CATransaction.setDisableActions(true)

        // 清除旧层
        for pageLayers in iconLayers {
            for layer in pageLayers {
                layer.removeFromSuperlayer()
            }
        }
        iconLayers.removeAll()

        guard !items.isEmpty else {
            CATransaction.commit()
            // print("⚠️ [CAGrid] rebuildLayers: no items")
            return
        }

        // 为每页创建图层
        let totalPages = pageCount
        // print("🔧 [CAGrid] rebuildLayers: \(items.count) items, \(totalPages) pages, \(itemsPerPage) per page")

        for pageIndex in 0..<totalPages {
            var pageLayers: [CALayer] = []
            let startIndex = pageIndex * itemsPerPage
            let endIndex = min(startIndex + itemsPerPage, items.count)

            for i in startIndex..<endIndex {
                let localIndex = i - startIndex
                let layer = createIconLayer(for: items[i], localIndex: localIndex, pageIndex: pageIndex)
                pageContainerLayer.addSublayer(layer)
                pageLayers.append(layer)
            }

            iconLayers.append(pageLayers)
        }

        CATransaction.commit()

        // Update layout if bounds are ready, otherwise layout() will handle it later
        if bounds.width > 0 && bounds.height > 0 {
            updateLayout()
        }

        // Navigate to current page (will be handled by layout() if bounds not ready)
        navigateToPage(currentPage, animated: false)
        logIfMismatch("rebuildLayers")
    }
    
    // Track if layout needs refresh after bounds become valid

    func createIconLayer(for item: LaunchpadItem, localIndex: Int, pageIndex: Int) -> CALayer {
        _ = localIndex
        _ = pageIndex
        let containerLayer = CALayer()
        containerLayer.masksToBounds = false
        containerLayer.drawsAsynchronously = true

        // For folders, add a glass background layer
        if case .folder = item {
            let glassLayer = CALayer()
            glassLayer.name = "glass"
            let glassStyle = currentFolderGlassStyle()
            glassLayer.backgroundColor = glassStyle.background.cgColor
            glassLayer.borderColor = glassStyle.border.cgColor
            glassLayer.borderWidth = 0.5
            glassLayer.masksToBounds = false
            // Subtle shadow
            glassLayer.shadowColor = NSColor.black.cgColor
            glassLayer.shadowOffset = glassStyle.shadowOffset
            glassLayer.shadowRadius = glassStyle.shadowRadius
            glassLayer.shadowOpacity = glassStyle.shadowOpacity
            containerLayer.addSublayer(glassLayer)
        }

        // Icon layer
        let iconLayer = CALayer()
        iconLayer.name = "icon"
        iconLayer.contentsGravity = .resizeAspect
        iconLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2.0
        iconLayer.masksToBounds = false
        iconLayer.shouldRasterize = true
        iconLayer.rasterizationScale = NSScreen.main?.backingScaleFactor ?? 2.0
        iconLayer.drawsAsynchronously = true
        iconLayer.shadowOpacity = 0

        containerLayer.addSublayer(iconLayer)

        // 文字标签层 - 匹配原 SwiftUI 样式
        let textLayer = CATextLayer()
        textLayer.name = "label"
        textLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2.0
        textLayer.fontSize = labelFontSize
        textLayer.font = NSFont.systemFont(ofSize: labelFontSize, weight: labelFontWeight)
        textLayer.isHidden = !showLabels
        textLayer.alignmentMode = .center
        textLayer.truncationMode = .end
        textLayer.isWrapped = false

        // 性能优化:栅格化文字层
        textLayer.shouldRasterize = true
        textLayer.rasterizationScale = NSScreen.main?.backingScaleFactor ?? 2.0

        // Match system label color for light/dark
        textLayer.foregroundColor = currentLabelColor().cgColor
        textLayer.shadowOpacity = 0

        // 设置文字内容
        switch item {
        case .app(let app):
            textLayer.string = app.name
        case .folder(let folder):
            textLayer.string = folder.name
        case .missingApp(let placeholder):
            textLayer.string = placeholder.displayName
        case .empty:
            textLayer.string = ""
        }

        containerLayer.addSublayer(textLayer)

        // 设置图标
        setIcon(for: iconLayer, item: item)

        if case .app = item {
            let checkboxLayer = CALayer()
            checkboxLayer.name = "batchSelectionCheckbox"
            checkboxLayer.isHidden = true
            checkboxLayer.cornerRadius = 8
            checkboxLayer.borderWidth = 1.25
            checkboxLayer.zPosition = 20
            checkboxLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2.0

            let markLayer = CAShapeLayer()
            markLayer.name = "batchSelectionCheckboxMark"
            markLayer.fillColor = NSColor.clear.cgColor
            markLayer.strokeColor = NSColor.white.cgColor
            markLayer.lineCap = .round
            markLayer.lineJoin = .round
            markLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2.0
            checkboxLayer.addSublayer(markLayer)
            containerLayer.addSublayer(checkboxLayer)
        }

        return containerLayer
    }

    func setIcon(for layer: CALayer, item: LaunchpadItem) {
        switch item {
        case .app(let app):
            let path = app.url.path
            layer.setValue(path, forKey: "iconPath")
            if let cgImage = getCachedIcon(for: app.url.path) {
                layer.contents = cgImage
            } else {
                // 异步加载 - 直接从系统获取图标
                DispatchQueue.global(qos: .userInitiated).async { [weak self, weak layer] in
                    guard let self = self, let layer = layer else { return }
                    guard layer.value(forKey: "iconPath") as? String == path else { return }
                    // 使用 IconStore 获取图标(CA 模式走 Next Engine 逻辑)
                    let icon = IconStore.shared.icon(forPath: path)
                    if let cgImage = self.loadIcon(for: path, icon: icon) {
                        DispatchQueue.main.async {
                            guard layer.value(forKey: "iconPath") as? String == path else { return }
                            CATransaction.begin()
                            CATransaction.setDisableActions(true)
                            layer.contents = cgImage
                            CATransaction.commit()
                        }
                    }
                }
            }
        case .folder(let folder):
            // 异步加载文件夹图标
            let folderIconSize = iconSize
            let previewScale = folderPreviewScale
            DispatchQueue.global(qos: .userInitiated).async { [weak layer] in
                let icon = folder.icon(of: folderIconSize, scale: previewScale)
                if let cgImage = icon.cgImage(forProposedRect: nil, context: nil, hints: nil) {
                    DispatchQueue.main.async {
                        CATransaction.begin()
                        CATransaction.setDisableActions(true)
                        layer?.contents = cgImage
                        CATransaction.commit()
                    }
                }
            }
        case .missingApp(let placeholder):
            if let cgImage = placeholder.icon.cgImage(forProposedRect: nil, context: nil, hints: nil) {
                layer.contents = cgImage
            }
        case .empty:
            layer.contents = nil
        }
    }

    func getCachedIcon(for path: String) -> CGImage? {
        iconCacheLock.lock()
        defer { iconCacheLock.unlock() }
        return iconCache[path]
    }

    func loadIcon(for path: String, icon: NSImage) -> CGImage? {
        iconCacheLock.lock()
        if let cached = iconCache[path] {
            iconCacheLock.unlock()
            return cached
        }
        iconCacheLock.unlock()

        // 渲染为 CGImage
        let size = NSSize(width: iconSize * 2, height: iconSize * 2) // Retina
        let image = NSImage(size: size)
        image.lockFocus()
        icon.draw(in: NSRect(origin: .zero, size: size))
        image.unlockFocus()

        guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
            return nil
        }

        iconCacheLock.lock()
        iconCache[path] = cgImage
        iconCacheLock.unlock()

        return cgImage
    }

    func preloadIcons() {
        guard enableIconPreload else { return }
        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
            guard let self = self else { return }
            for item in self.items {
                if case .app(let app) = item {
                    let icon = IconStore.shared.icon(forPath: app.url.path)
                    _ = self.loadIcon(for: app.url.path, icon: icon)
                }
            }
            // print("✅ [CAGrid] Icons preloaded")
        }
    }

    // MARK: - Layout

    func updateLayout() {
        guard bounds.width > 0, bounds.height > 0 else { return }

        CATransaction.begin()
        CATransaction.setDisableActions(true)

        let pageWidth = bounds.width
        let pageHeight = bounds.height
        let pageStride = pageWidth + pageSpacing

        let availableWidth = max(0, pageWidth - contentInsets.left - contentInsets.right)
        let availableHeight = max(0, pageHeight - contentInsets.top - contentInsets.bottom)

        let totalColumnSpacing = columnSpacing * CGFloat(max(columns - 1, 0))
        let totalRowSpacing = rowSpacing * CGFloat(max(rows - 1, 0))
        let usableWidth = max(0, availableWidth - totalColumnSpacing)
        let usableHeight = max(0, availableHeight - totalRowSpacing)
        let cellWidth = usableWidth / CGFloat(max(columns, 1))
        let cellHeight = usableHeight / CGFloat(max(rows, 1))
        let strideX = cellWidth + columnSpacing

        let actualIconSize = iconSize
        let labelHeight: CGFloat = showLabels ? (labelFontSize + 8) : 0
        let labelTopSpacing: CGFloat = showLabels ? 6 : 0

        for (pageIndex, pageLayers) in iconLayers.enumerated() {
            for (localIndex, containerLayer) in pageLayers.enumerated() {
                let col = localIndex % columns
                let row = localIndex / columns

                let cellOriginX = contentInsets.left + CGFloat(col) * strideX
                let cellOriginY = pageHeight - contentInsets.top - CGFloat(row + 1) * cellHeight - CGFloat(row) * rowSpacing

                let totalHeight = actualIconSize + labelTopSpacing + labelHeight
                let containerX = CGFloat(pageIndex) * pageStride + cellOriginX
                let containerY = cellOriginY + (cellHeight - totalHeight) / 2

                containerLayer.frame = CGRect(x: containerX, y: containerY, width: cellWidth, height: totalHeight)

                if let iconLayer = containerLayer.sublayers?.first(where: { $0.name == "icon" }) {
                    let iconX = (cellWidth - actualIconSize) / 2
                    let iconY = labelHeight + labelTopSpacing
                    let iconFrame = CGRect(x: iconX, y: iconY, width: actualIconSize, height: actualIconSize)
                    iconLayer.frame = iconFrame

                    if let checkboxLayer = containerLayer.sublayers?.first(where: { $0.name == "batchSelectionCheckbox" }) {
                        let checkboxSize = max(16, min(22, actualIconSize * 0.28))
                        let edgeInset = max(2.5, min(5.0, actualIconSize * 0.055))
                        let checkboxX = iconFrame.maxX - checkboxSize - edgeInset
                        let checkboxY = iconFrame.maxY - checkboxSize - edgeInset
                        checkboxLayer.frame = CGRect(x: checkboxX, y: checkboxY, width: checkboxSize, height: checkboxSize)
                        checkboxLayer.cornerRadius = checkboxSize * 0.5
                        if let markLayer = checkboxLayer.sublayers?.first(where: { $0.name == "batchSelectionCheckboxMark" }) as? CAShapeLayer {
                            markLayer.frame = checkboxLayer.bounds
                            markLayer.lineWidth = max(1.7, checkboxSize * 0.14)
                            markLayer.path = checkboxMarkPath(in: checkboxLayer.bounds)
                        }
                    }
                }
                
                // Update glass background for folders
                if let glassLayer = containerLayer.sublayers?.first(where: { $0.name == "glass" }) {
                    let glassSize = actualIconSize * 0.8
                    let glassX = (cellWidth - glassSize) / 2
                    let glassY = labelHeight + labelTopSpacing + (actualIconSize - glassSize) / 2
                    glassLayer.frame = CGRect(x: glassX, y: glassY, width: glassSize, height: glassSize)
                    glassLayer.cornerRadius = glassSize * 0.25  // Larger corner radius
                }

                if let textLayer = containerLayer.sublayers?.first(where: { $0.name == "label" }) as? CATextLayer {
                    let labelWidth = cellWidth - 8
                    textLayer.isHidden = !showLabels
                    textLayer.frame = CGRect(x: 4, y: 0, width: labelWidth, height: labelHeight)
                }
            }
        }

        let totalWidth = pageWidth * CGFloat(max(1, pageCount)) + pageSpacing * CGFloat(max(pageCount - 1, 0))
        // Avoid setting frame on a transformed layer; reset to identity, update frame, then re-apply translation.
        // This prevents visual/data misalignment caused by frame updates under non-identity transforms.
        pageContainerLayer.transform = CATransform3DIdentity
        pageContainerLayer.frame = CGRect(x: 0, y: 0, width: totalWidth, height: bounds.height)
        pageContainerLayer.transform = CATransform3DMakeTranslation(scrollOffset, 0, 0)
        refreshBatchSelectionUI()

        CATransaction.commit()

        logIfMismatch("updateLayout")
        // print("📐 [CAGrid] Layout: \(columns)x\(rows), iconSize=\(actualIconSize), cell=\(cellWidth)x\(cellHeight)")
    }

    func refreshBatchSelectionUI() {
        CATransaction.begin()
        CATransaction.setDisableActions(true)

        for (pageIndex, pageLayers) in iconLayers.enumerated() {
            let pageStart = pageIndex * itemsPerPage
            for (localIndex, containerLayer) in pageLayers.enumerated() {
                let globalIndex = pageStart + localIndex
                guard items.indices.contains(globalIndex),
                      case .app(let app) = items[globalIndex],
                      let checkboxLayer = containerLayer.sublayers?.first(where: { $0.name == "batchSelectionCheckbox" }) else {
                    continue
                }
                let isSelected = batchSelectedAppPathSet.contains(app.url.path)
                checkboxLayer.isHidden = !isBatchSelectionMode
                if isSelected {
                    checkboxLayer.backgroundColor = NSColor.systemBlue.cgColor
                    checkboxLayer.borderColor = NSColor.systemBlue.withAlphaComponent(0.95).cgColor
                } else {
                    checkboxLayer.backgroundColor = NSColor.white.withAlphaComponent(0.88).cgColor
                    checkboxLayer.borderColor = NSColor.black.withAlphaComponent(0.35).cgColor
                }
                if let markLayer = checkboxLayer.sublayers?.first(where: { $0.name == "batchSelectionCheckboxMark" }) as? CAShapeLayer {
                    markLayer.isHidden = !isSelected
                    markLayer.strokeColor = NSColor.white.cgColor
                }
            }
        }

        CATransaction.commit()
    }

    func checkboxMarkPath(in bounds: CGRect) -> CGPath {
        let path = CGMutablePath()
        let start = CGPoint(x: bounds.minX + bounds.width * 0.24, y: bounds.minY + bounds.height * 0.50)
        let mid = CGPoint(x: bounds.minX + bounds.width * 0.44, y: bounds.minY + bounds.height * 0.30)
        let end = CGPoint(x: bounds.minX + bounds.width * 0.76, y: bounds.minY + bounds.height * 0.66)
        path.move(to: start)
        path.addLine(to: mid)
        path.addLine(to: end)
        return path
    }

    override func viewWillDraw() {
        super.viewWillDraw()
        // 确保视图是第一响应者和滚轮监听器已安装
        if window != nil {
            makeFirstResponderIfAvailable()
            // 确保滚轮监听器存在
            if scrollEventMonitor == nil {
                setupScrollEventMonitor()
            }
        }
    }

    override func layout() {
        super.layout()

        guard bounds.width > 0, bounds.height > 0 else { return }

        CATransaction.begin()
        CATransaction.setDisableActions(true)
        containerLayer.frame = bounds
        CATransaction.commit()

        // Force layout update, especially important when bounds become valid after items were set
        updateLayout()
        needsLayoutRefresh = false

        // Clamp currentPage to valid range after items/pageCount might have changed
        let validPage = max(0, min(pageCount - 1, currentPage))
        if validPage != currentPage {
            currentPage = validPage
        }

        // Reposition to current page without animation
        let pageStride = bounds.width + pageSpacing
        scrollOffset = -CGFloat(currentPage) * pageStride
        targetScrollOffset = scrollOffset
        isScrollAnimating = false

        CATransaction.begin()
        CATransaction.setDisableActions(true)
        pageContainerLayer.transform = CATransform3DMakeTranslation(scrollOffset, 0, 0)
        CATransaction.commit()
        logIfMismatch("layout")
    }

}

```

## /LaunchNext/CAGridView.swift

```swift path="/LaunchNext/CAGridView.swift" 
import AppKit
import QuartzCore
import Combine
import SwiftUI

// MARK: - Safe Array Subscript
extension Array {
    subscript(safe index: Int) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

// MARK: - Core Animation Grid View
/// 使用 Core Animation 实现的高性能网格视图,支持 120Hz ProMotion
final class CAGridView: NSView, CALayerDelegate, NSDraggingSource {

    // MARK: - Properties

    var displayLink: CADisplayLink?
    var containerLayer: CALayer!
    var pageContainerLayer: CALayer!
    var iconLayers: [[CALayer]] = []  // [page][item]

    // 网格配置
    var columns: Int = 7 { didSet { rebuildLayers() } }
    var rows: Int = 5 { didSet { rebuildLayers() } }
    var iconSize: CGFloat = 72 {
        didSet {
            guard iconSize != oldValue else { return }
            clearIconCache()
            updateLayout()
        }
    }
    var columnSpacing: CGFloat = 24 { didSet { updateLayout() } }
    var rowSpacing: CGFloat = 36 { didSet { updateLayout() } }
    var labelFontSize: CGFloat = 12 { didSet { rebuildLayers() } }  // 默认 12pt,比原来大一点
    var labelFontWeight: NSFont.Weight = .medium { didSet { updateLabelFonts() } }
    var showLabels: Bool = true { didSet { updateLabelVisibility() } }
    var isLayoutLocked: Bool = false
    var folderDropZoneScale: CGFloat = CGFloat(AppStore.defaultFolderDropZoneScale)
    var folderPreviewScale: CGFloat = 1
    var contentInsets: NSEdgeInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) { didSet { updateLayout() } }
    var pageSpacing: CGFloat = 0 { didSet { updateLayout() } }

    // Data source
    var items: [LaunchpadItem] = [] {
        didSet {
            needsLayoutRefresh = true
            syncBatchSelectionWithItems()
            rebuildLayers()
            if enableIconPreload {
                preloadIcons()
            }
        }
    }
    var needsLayoutRefresh = true

    // 分页
    var currentPage: Int = 0
    var itemsPerPage: Int { columns * rows }
    var pageCount: Int { max(1, (items.count + itemsPerPage - 1) / itemsPerPage) }

    // 滚动状态
    var scrollOffset: CGFloat = 0
    var targetScrollOffset: CGFloat = 0
    var scrollVelocity: CGFloat = 0
    var isScrollAnimating = false
    var scrollSensitivity: Double = AppStore.defaultScrollSensitivity
    var reverseWheelPagingDirection: Bool = false
    var animationsEnabled: Bool = true
    var animationDuration: Double = 0.3
    var scrollAnimationStartTime: CFTimeInterval = 0
    var scrollAnimationStartOffset: CGFloat = 0
    var hoverMagnificationEnabled: Bool = false {
        didSet {
            if !hoverMagnificationEnabled {
                clearHover()
            }
        }
    }
    var hoverMagnificationScale: CGFloat = 1.2
    var activePressEffectEnabled: Bool = false
    var activePressScale: CGFloat = 0.92
    var isDragging = false
    var dragStartOffset: CGFloat = 0
    var accumulatedDelta: CGFloat = 0

    // 性能监控
    var lastFrameTime: CFAbsoluteTime = 0
    var frameCount: Int = 0
    var currentFPS: Double = 120
    var frameTimes: [Double] = []

    // 图标缓存
    var iconCache: [String: CGImage] = [:]
    let iconCacheLock = NSLock()
    var enableIconPreload: Bool = false

    // 回调
    var onItemClicked: ((LaunchpadItem, Int) -> Void)?
    var onItemDoubleClicked: ((LaunchpadItem, Int) -> Void)?
    var onPageChanged: ((Int) -> Void)?
    var onFPSUpdate: ((Double) -> Void)?
    var onEmptyAreaClicked: (() -> Void)?
    var onShowAppInFinder: ((AppInfo) -> Void)?
    var onCopyAppPath: ((AppInfo) -> Void)?
    var onHideApp: ((AppInfo) -> Void)?
    var onRenameFolder: ((FolderInfo) -> Void)?
    var onDissolveFolder: ((FolderInfo) -> Void)?
    var onUninstallWithTool: ((AppInfo) -> Void)?
    var onCreateFolder: ((AppInfo, AppInfo, Int) -> Void)?  // (拖拽的app, 目标app, 位置)
    var onMoveToFolder: ((AppInfo, FolderInfo) -> Void)?    // 移动到已有文件夹
    var onReorderItems: ((Int, Int) -> Void)?               // 重新排序 (fromIndex, toIndex)
    var onReorderAppBatch: (([String], Int) -> Void)?       // 批量重排(按路径顺序)
    var onRequestNewPage: (() -> Void)?                     // 请求创建新页面
    var showInFinderMenuTitle: String = "Show in Finder"
    var copyAppPathMenuTitle: String = "Copy App Path"
    var hideAppMenuTitle: String = "Hide application"
    var renameFolderMenuTitle: String = "Rename Folder"
    var dissolveFolderMenuTitle: String = "Dissolve folder"
    var uninstallWithToolMenuTitle: String = "Uninstall with configured tool"
    var batchSelectAppsMenuTitle: String = "Batch Select Apps"
    var finishBatchSelectionMenuTitle: String = "Finish Batch Selection"
    var canUseConfiguredUninstallTool: Bool = false
    var contextMenuTargetApp: AppInfo?
    var contextMenuTargetFolder: FolderInfo?
    var allowsBatchSelectionMode: Bool = true {
        didSet {
            if !allowsBatchSelectionMode {
                disableBatchSelectionMode()
            }
        }
    }
    var isBatchSelectionMode = false
    var batchSelectedAppPathsOrdered: [String] = []
    var batchSelectedAppPathSet: Set<String> = []
    var batchDraggingAppPathsOrdered: [String] = []
    var batchHiddenCompanionIndices: [Int] = []

    // 拖拽状态
    var isDraggingItem = false
    var draggingIndex: Int?
    var draggingItem: LaunchpadItem?
    var draggingLayer: CALayer?
    var dragStartPoint: CGPoint = .zero
    var dragCurrentPoint: CGPoint = .zero
    var dropTargetIndex: Int?
    var longPressTimer: Timer?
    let longPressDuration: TimeInterval = 0.5
    var pressedIndex: Int?

    // 跨页拖拽
    var edgeDragTimer: Timer?
    let edgeDragThreshold: CGFloat = 60  // 边缘检测区域宽度
    let edgeDragDelay: TimeInterval = 0.4  // 触发翻页延迟

    // Live reorder during drag
    var currentHoverIndex: Int?
    var pendingHoverIndex: Int?
    var originalIconPositions: [Int: CGPoint] = [:]
    var hoverUpdateTimer: Timer?
    let hoverUpdateDelay: TimeInterval = 0.15  // Delay before updating icon positions

    // 鼠标拖拽翻页
    var isPageDragging = false
    var pageDragStartX: CGFloat = 0
    var pageDragStartOffset: CGFloat = 0

    // 事件监听器
    var scrollEventMonitor: Any?
    var wasWindowVisible = false  // 跟踪窗口可见状态
    
    // 鼠标滚轮分页状态(仅用于非精准滚动设备)
    var wheelAccumulatedDelta: CGFloat = 0
    var wheelLastDirection: Int = 0
    var wheelLastFlipAt: Date?
    let wheelFlipCooldown: TimeInterval = 0.15
    // Legacy reference:
    // var wheelSnapTimer: Timer?
    // let wheelSnapDelay: TimeInterval = 0.15  // 停止滚动后多久触发 snap
    let debugScrollMismatch = false
    var externalDragActive = false
    var externalAppDragSessionActive = false
    var hoveredIndex: Int?
    var selectedIndex: Int?
    var hoverTrackingArea: NSTrackingArea?
    var isScrollEnabled: Bool = true
    var dockDragEnabled: Bool = true
    let externalAppDragOutset: CGFloat = 18
    var dockDragSide: AppStore.DockDragSide = .bottom
    var externalAppDragTriggerDistance: CGFloat = CGFloat(AppStore.defaultDockDragTriggerDistance)

    func logIfMismatch(_ tag: String, appPage: Int? = nil) {
        guard debugScrollMismatch else { return }
        guard bounds.width > 0 else { return }
        let pageStride = bounds.width + pageSpacing
        let expectedOffset = -CGFloat(currentPage) * pageStride
        let transformOffset = pageContainerLayer.transform.m41
        let offsetMismatch = abs(scrollOffset - expectedOffset) > 0.5
        let transformMismatch = abs(transformOffset - scrollOffset) > 0.5
        guard offsetMismatch || transformMismatch else { return }
        let appInfo = appPage.map { ", appPage=\($0)" } ?? ""
        // print("⚠️ [CAGrid #\(instanceId)] \(tag) mismatch: currentPage=\(currentPage)\(appInfo), scroll=\(scrollOffset), expected=\(expectedOffset), transform=\(transformOffset), boundsW=\(bounds.width), pageSpacing=\(pageSpacing)")
    }

    // 实例追踪
    private static var instanceCounter = 0
    let instanceId: Int

    // MARK: - Initialization

    override init(frame frameRect: NSRect) {
        CAGridView.instanceCounter += 1
        self.instanceId = CAGridView.instanceCounter
        super.init(frame: frameRect)
        setup()
    }

    required init?(coder: NSCoder) {
        CAGridView.instanceCounter += 1
        self.instanceId = CAGridView.instanceCounter
        super.init(coder: coder)
        setup()
    }

    deinit {
        // print("💀 [CAGrid #\(instanceId)] deinit - instance being destroyed!")
        displayLink?.invalidate()
        removeScrollEventMonitor()
        NotificationCenter.default.removeObserver(self)
    }

    func setup() {
        wantsLayer = true
        layerContentsRedrawPolicy = .onSetNeedsDisplay

        // 创建容器层
        containerLayer = CALayer()
        containerLayer.frame = bounds
        containerLayer.masksToBounds = false  // 不裁剪,让滑动时内容可以超出边界
        layer?.addSublayer(containerLayer)

        // 页面容器层(用于整体偏移)
        pageContainerLayer = CALayer()
        pageContainerLayer.frame = bounds
        containerLayer.addSublayer(pageContainerLayer)

        // 禁用隐式动画
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        CATransaction.commit()

        // 在初始化时就注册 launchpad 窗口通知(确保始终能接收)
        NotificationCenter.default.addObserver(self, selector: #selector(launchpadWindowDidShow(_:)), name: .launchpadWindowShown, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(launchpadWindowDidHide(_:)), name: .launchpadWindowHidden, object: nil)
        // 监听应用激活事件(作为备用方案)
        NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(_:)), name: NSApplication.didBecomeActiveNotification, object: nil)

        // print("✅ [CAGrid #\(instanceId)] Core Animation grid initialized")
    }

    func makeFirstResponderIfAvailable() {
        guard let win = window else { return }
        if win.firstResponder == nil || win.firstResponder === self {
            win.makeFirstResponder(self)
        }
    }

    override func viewDidMoveToWindow() {
        super.viewDidMoveToWindow()
        if let window = window {
            window.acceptsMouseMovedEvents = true
            setupDisplayLink()
            // 始终安装滚轮事件监听器(更可靠)
            setupScrollEventMonitor()
            // 确保视图成为第一响应者
            DispatchQueue.main.async { [weak self] in
                self?.makeFirstResponderIfAvailable()
            }
            // print("✅ [CAGrid #\(instanceId)] View moved to window, scroll monitor installed")

            // 监听窗口显示/隐藏事件
            NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeKeyNotification, object: nil)
            NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: nil)
            NotificationCenter.default.removeObserver(self, name: NSWindow.didChangeOcclusionStateNotification, object: nil)

            NotificationCenter.default.addObserver(self, selector: #selector(windowDidActivate(_:)), name: NSWindow.didBecomeKeyNotification, object: window)
            NotificationCenter.default.addObserver(self, selector: #selector(windowDidActivate(_:)), name: NSWindow.didBecomeMainNotification, object: window)
            NotificationCenter.default.addObserver(self, selector: #selector(windowOcclusionChanged(_:)), name: NSWindow.didChangeOcclusionStateNotification, object: window)
            // launchpad 窗口通知在 setup() 中注册,这里不需要重复注册
        } else {
            // 视图从窗口移除时清理窗口相关的事件监听器
            // 注意:launchpad 窗口通知不在这里移除,因为它们在 setup() 中注册
            removeScrollEventMonitor()
            NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeKeyNotification, object: nil)
            NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: nil)
            NotificationCenter.default.removeObserver(self, name: NSWindow.didChangeOcclusionStateNotification, object: nil)
        }
    }

    @objc func windowDidActivate(_ notification: Notification) {
        // print("🪟 [CAGrid] Window activated, making first responder")
        makeFirstResponderIfAvailable()
    }

    @objc func windowOcclusionChanged(_ notification: Notification) {
        guard let window = window else { return }
        if window.occlusionState.contains(.visible) {
            // print("🪟 [CAGrid] Window became visible, making first responder")
            makeFirstResponderIfAvailable()
        }
    }

    @objc func launchpadWindowDidShow(_ notification: Notification) {
        // 只有有窗口的实例才响应
        guard let window = window else {
            // print("⚠️ [CAGrid #\(instanceId)] Launchpad window shown - but no window, ignoring")
            return
        }
        // print("🚀 [CAGrid #\(instanceId)] Launchpad window shown, hasMonitor=\(scrollEventMonitor != nil)")

        // 立即安装滚轮事件监听器(如果没有)
        if scrollEventMonitor == nil {
            // print("🔄 [CAGrid #\(instanceId)] Reinstalling scroll monitor on window show")
            setupScrollEventMonitor()
        }

        // 确保成为第一响应者
        makeFirstResponderIfAvailable()

        // 延迟再次确认(防止其他组件抢占)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
            guard let self = self, let win = self.window else { return }
            // print("🔄 [CAGrid #\(self.instanceId)] Delayed check, isFirstResponder=\(win.firstResponder === self), hasMonitor=\(self.scrollEventMonitor != nil)")
            self.makeFirstResponderIfAvailable()
            // 确保滚轮监听器存在
            if self.scrollEventMonitor == nil {
                self.setupScrollEventMonitor()
            }
        }
    }

    @objc func launchpadWindowDidHide(_ notification: Notification) {
        // 只有有窗口的实例才响应
        guard window != nil else {
            // print("⚠️ [CAGrid #\(instanceId)] Window hidden - but no window, ignoring")
            return
        }
        // print("🚀 [CAGrid #\(instanceId)] Window hidden, hasMonitor=\(scrollEventMonitor != nil)")
        // 不再移除监听器 - 让它保持活跃,这样窗口重新显示时就能立即使用
        // removeScrollEventMonitor()
        wasWindowVisible = false
    }

    @objc func appDidBecomeActive(_ notification: Notification) {
        // 应用激活时检查是否需要安装滚轮监听器
        // print("🔔 [CAGrid #\(instanceId)] App became active notification received, window=\(window != nil), isVisible=\(window?.isVisible ?? false)")
        guard let window = window else {
            // print("🔔 [CAGrid #\(instanceId)] App became active - no window")
            return
        }

        // 立即尝试重新安装滚轮监听器(不管窗口是否可见)
        // 因为窗口可能正在动画中,isVisible 可能还是 false
        // print("🔔 [CAGrid #\(instanceId)] Reinstalling scroll monitor immediately on app activate")
        setupScrollEventMonitor()
        makeFirstResponderIfAvailable()

        // 延迟再次检查,确保滚轮监听器存在
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
            guard let self = self, let win = self.window else { return }
            // print("🔔 [CAGrid #\(self.instanceId)] Delayed check: isVisible=\(win.isVisible), scrollMonitor=\(self.scrollEventMonitor != nil)")
            if self.scrollEventMonitor == nil {
                // print("🔄 [CAGrid #\(self.instanceId)] App became active (delayed), reinstalling scroll monitor")
                self.setupScrollEventMonitor()
            }
            self.makeFirstResponderIfAvailable()
        }
    }

    func setupScrollEventMonitor() {
        // 移除旧的监听器
        removeScrollEventMonitor()

        // 确保有窗口才设置监听器(可见性在事件处理时动态检查)
        guard window != nil else {
            // print("⚠️ [CAGrid #\(instanceId)] setupScrollEventMonitor: no window, skipping")
            return
        }

        let myInstanceId = self.instanceId

        scrollEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { [weak self] event in
            guard let self = self else { return event }
            guard self.isScrollEnabled else { return event }
            guard let window = self.window else { return event }
            
            // 只在窗口可见且是 key window 时处理
            guard window.isVisible && window.isKeyWindow else { return event }
            
            // 检查事件是否在视图范围内
            let locationInWindow = event.locationInWindow
            let locationInView = self.convert(locationInWindow, from: nil)
            guard self.bounds.contains(locationInView) else { return event }
            
            self.handleScrollWheel(with: event)
            // 消费事件,不再传递,防止双重处理
            return nil
        }
        // print("✅ [CAGrid #\(instanceId)] Scroll event monitor installed")
    }

    func removeScrollEventMonitor() {
        if let monitor = scrollEventMonitor {
            // print("🗑️ [CAGrid #\(instanceId)] Removing scroll event monitor")
            NSEvent.removeMonitor(monitor)
            scrollEventMonitor = nil
        }
    }

    // MARK: - Display Link (120Hz)

    func setupDisplayLink() {
        displayLink?.invalidate()

        guard let window = window else {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
                self?.setupDisplayLink()
            }
            return
        }

        displayLink = window.displayLink(target: self, selector: #selector(displayLinkFired(_:)))
        displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60, maximum: 120, preferred: 120)
        displayLink?.add(to: .main, forMode: .common)

        // print("✅ [CAGrid] DisplayLink configured for 120Hz")
    }

    @objc func displayLinkFired(_ link: CADisplayLink) {
        // 只在动画时才更新
        guard isScrollAnimating || isDraggingItem else {
            // 空闲时重置帧计数
            if frameCount > 0 {
                frameCount = 0
                lastFrameTime = 0
            }
            return
        }

        // 计算实时帧率(仅在动画时)
        let now = CFAbsoluteTimeGetCurrent()
        if lastFrameTime > 0 {
            let delta = now - lastFrameTime
            let instantFPS = 1.0 / delta
            // 使用滑动窗口平均,减少数组操作
            if frameTimes.count >= 30 {
                frameTimes.removeFirst()
            }
            frameTimes.append(instantFPS)
            currentFPS = frameTimes.reduce(0, +) / Double(frameTimes.count)
        }
        lastFrameTime = now

        frameCount += 1
        // 每 60 帧输出一次(约 0.5 秒)
        if frameCount % 60 == 0 {
            onFPSUpdate?(currentFPS)
            // print("🎮 [CAGrid] Avg FPS: \(String(format: "%.1f", currentFPS))")
        }

        // 更新滚动动画
        if isScrollAnimating {
            updateScrollAnimation()
        }
    }

    // MARK: - Scroll Animation

    func updateScrollAnimation() {
        if !animationsEnabled {
            scrollOffset = targetScrollOffset
            scrollVelocity = 0
            isScrollAnimating = false
        } else {
            let diff = targetScrollOffset - scrollOffset
            let snapThreshold: CGFloat = 0.5
            if abs(diff) > snapThreshold {
                // 非时间控制:指数收敛,距离越远移动越快
                let t: CGFloat = 0.18
                scrollOffset += diff * t
            } else {
                scrollOffset = targetScrollOffset
                scrollVelocity = 0
                isScrollAnimating = false
            }
        }

        // 更新页面容器位置 - 使用最小开销的方式
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        CATransaction.setAnimationDuration(0)
        pageContainerLayer.transform = CATransform3DMakeTranslation(scrollOffset, 0, 0)
        CATransaction.commit()
    }

    func easeOutSoft(_ t: CGFloat) -> CGFloat {
        // Damped spring: soft ease-out with a subtle bounce (about ~2% overshoot).
        return springEaseOut(t, damping: 0.78, frequency: 1.4)
    }

    func springEaseOut(_ t: CGFloat, damping: CGFloat, frequency: CGFloat) -> CGFloat {
        let clamped = max(0, min(1, t))
        if clamped == 0 { return 0 }
        if clamped == 1 { return 1 }

        let dampingRatio = max(0, min(1, damping))
        let omega = 2 * CGFloat.pi * max(0.1, frequency)
        if dampingRatio >= 1 {
            return 1 - exp(-omega * clamped)
        }

        let omegaD = omega * sqrt(1 - dampingRatio * dampingRatio)
        let expTerm = exp(-dampingRatio * omega * clamped)
        let cosTerm = cos(omegaD * clamped)
        let sinTerm = sin(omegaD * clamped)
        let coeff = dampingRatio / sqrt(1 - dampingRatio * dampingRatio)
        return 1 - expTerm * (cosTerm + coeff * sinTerm)
    }

    func easeOutBack(_ t: CGFloat, overshoot: CGFloat) -> CGFloat {
        let clamped = max(0, min(1, t))
        let s = max(0, overshoot)
        let t1 = clamped - 1
        return 1 + (s + 1) * t1 * t1 * t1 + s * t1 * t1
    }

    func cubicBezier(_ x: CGFloat, c1: CGPoint, c2: CGPoint) -> CGFloat {
        let clamped = max(0, min(1, x))
        let t = solveBezierT(forX: clamped, c1x: c1.x, c2x: c2.x)
        return cubicBezierValue(t, c1: c1.y, c2: c2.y)
    }

    func cubicBezierValue(_ t: CGFloat, c1: CGFloat, c2: CGFloat) -> CGFloat {
        let oneMinusT = 1 - t
        return 3 * oneMinusT * oneMinusT * t * c1
            + 3 * oneMinusT * t * t * c2
            + t * t * t
    }

    func cubicBezierDerivative(_ t: CGFloat, c1: CGFloat, c2: CGFloat) -> CGFloat {
        let oneMinusT = 1 - t
        return 3 * oneMinusT * oneMinusT * c1
            + 6 * oneMinusT * t * (c2 - c1)
            + 3 * t * t * (1 - c2)
    }

    func solveBezierT(forX x: CGFloat, c1x: CGFloat, c2x: CGFloat) -> CGFloat {
        var t = x
        for _ in 0..<5 {
            let xAtT = cubicBezierValue(t, c1: c1x, c2: c2x)
            let dx = xAtT - x
            if abs(dx) < 1e-4 { return t }
            let d = cubicBezierDerivative(t, c1: c1x, c2: c2x)
            if abs(d) < 1e-5 { break }
            t -= dx / d
            if t < 0 || t > 1 { break }
        }
        var low: CGFloat = 0
        var high: CGFloat = 1
        for _ in 0..<8 {
            let mid = (low + high) * 0.5
            let xAtMid = cubicBezierValue(mid, c1: c1x, c2: c2x)
            if xAtMid < x {
                low = mid
            } else {
                high = mid
            }
        }
        return (low + high) * 0.5
    }
    
    // Set initial page before items are set to ensure correct positioning
    func setInitialPage(_ page: Int) {
        currentPage = max(0, page)
    }

    func navigateToPage(_ page: Int, animated: Bool = true) {
        let newPage = max(0, min(pageCount - 1, page))
        let pageChanged = newPage != currentPage
        currentPage = newPage

        // 如果 bounds 还没准备好,只更新 currentPage,实际滚动交给 layout() 处理
        guard bounds.width > 0 else {
            if pageChanged {
                onPageChanged?(currentPage)
            }
            return
        }

        let pageStride = bounds.width + pageSpacing
        targetScrollOffset = -CGFloat(currentPage) * pageStride

        // 检查是否需要动画(包括弹回原位的情况)
        let needsAnimation = animated && abs(scrollOffset - targetScrollOffset) > 0.5
        
        if needsAnimation && animationsEnabled {
            isScrollAnimating = true
        } else {
            // 立即跳转
            isScrollAnimating = false
            scrollOffset = targetScrollOffset
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            pageContainerLayer.transform = CATransform3DMakeTranslation(scrollOffset, 0, 0)
            CATransaction.commit()
        }

        if pageChanged {
            onPageChanged?(currentPage)
        }
        logIfMismatch("navigateToPage")
    }

    // MARK: - Public Methods

    var isBatchDragging: Bool { !batchDraggingAppPathsOrdered.isEmpty }

    func enableBatchSelectionMode() {
        guard allowsBatchSelectionMode else {
            NSSound.beep()
            return
        }
        isBatchSelectionMode = true
        syncBatchSelectionWithItems()
        refreshBatchSelectionUI()
    }

    func disableBatchSelectionMode() {
        if isDraggingItem && isBatchDragging {
            cancelDragging()
        }
        let hadState = isBatchSelectionMode ||
            !batchSelectedAppPathsOrdered.isEmpty ||
            !batchDraggingAppPathsOrdered.isEmpty
        isBatchSelectionMode = false
        batchSelectedAppPathsOrdered.removeAll()
        batchSelectedAppPathSet.removeAll()
        batchDraggingAppPathsOrdered.removeAll()
        restoreBatchHiddenCompanionLayers()
        if hadState {
            refreshBatchSelectionUI()
        }
    }

    func toggleBatchSelection(forAppPath path: String) {
        guard isBatchSelectionMode else { return }
        if batchSelectedAppPathSet.contains(path) {
            batchSelectedAppPathSet.remove(path)
            batchSelectedAppPathsOrdered.removeAll { $0 == path }
        } else {
            batchSelectedAppPathSet.insert(path)
            batchSelectedAppPathsOrdered.append(path)
        }
        refreshBatchSelectionUI()
    }

    func orderedBatchDragPaths(leadingAppPath path: String) -> [String] {
        guard batchSelectedAppPathSet.contains(path) else { return [] }
        var ordered: [String] = [path]
        ordered.append(contentsOf: batchSelectedAppPathsOrdered.filter { $0 != path })
        return ordered
    }

    func appPath(at index: Int) -> String? {
        guard items.indices.contains(index), case .app(let app) = items[index] else { return nil }
        return app.url.path
    }

    func syncBatchSelectionWithItems() {
        guard isBatchSelectionMode else { return }
        let currentPaths = Set(items.compactMap { item -> String? in
            guard case .app(let app) = item else { return nil }
            return app.url.path
        })
        let oldCount = batchSelectedAppPathSet.count
        batchSelectedAppPathSet = batchSelectedAppPathSet.intersection(currentPaths)
        batchSelectedAppPathsOrdered = batchSelectedAppPathsOrdered.filter { batchSelectedAppPathSet.contains($0) }
        if batchSelectedAppPathSet.count != oldCount {
            refreshBatchSelectionUI()
        }
    }

    func globalIndex(forAppPath path: String) -> Int? {
        for (index, item) in items.enumerated() {
            if case .app(let app) = item, app.url.path == path {
                return index
            }
        }
        return nil
    }

    func setOpacity(_ opacity: Float, forGlobalIndex index: Int) {
        let pageIndex = index / itemsPerPage
        let localIndex = index % itemsPerPage
        guard pageIndex < iconLayers.count, localIndex < iconLayers[pageIndex].count else { return }
        iconLayers[pageIndex][localIndex].opacity = opacity
    }

    func restoreBatchHiddenCompanionLayers() {
        guard !batchHiddenCompanionIndices.isEmpty else { return }
        for index in batchHiddenCompanionIndices {
            setOpacity(1.0, forGlobalIndex: index)
        }
        batchHiddenCompanionIndices.removeAll()
        batchDraggingAppPathsOrdered.removeAll()
    }

    func clearIconCache() {
        iconCacheLock.lock()
        iconCache.removeAll()
        iconCacheLock.unlock()
    }

    func refreshLayout() {
        rebuildLayers()
    }

    func snapToCurrentPageIfNeeded() {
        // 如果用户正在拖拽或动画正在进行,不要强制 snap
        guard !isDragging && !isScrollAnimating && !isPageDragging else { return }
        guard bounds.width > 0 else { return }
        
        let expectedOffset = -CGFloat(currentPage) * (bounds.width + pageSpacing)
        let transformOffset = pageContainerLayer.transform.m41
        let needsOffsetSync = abs(scrollOffset - expectedOffset) > 0.5
        let needsTransformSync = abs(transformOffset - scrollOffset) > 0.5
        guard needsOffsetSync || needsTransformSync else { return }

        if needsOffsetSync {
            scrollOffset = expectedOffset
            targetScrollOffset = expectedOffset
        }

        CATransaction.begin()
        CATransaction.setDisableActions(true)
        CATransaction.setAnimationDuration(0)
        pageContainerLayer.transform = CATransform3DMakeTranslation(scrollOffset, 0, 0)
        CATransaction.commit()
    }

    func forceSyncPageTransformIfNeeded() {
        guard bounds.width > 0 else { return }
        let pageStride = bounds.width + pageSpacing
        let expectedOffset = -CGFloat(currentPage) * pageStride
        if abs(scrollOffset - expectedOffset) > 0.5 {
            scrollOffset = expectedOffset
            targetScrollOffset = expectedOffset
        }
        let transformOffset = pageContainerLayer.transform.m41
        guard abs(transformOffset - scrollOffset) > 0.5 else { return }
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        CATransaction.setAnimationDuration(0)
        pageContainerLayer.transform = CATransform3DMakeTranslation(scrollOffset, 0, 0)
        CATransaction.commit()
    }

    /// 确保滚轮事件监听器已安装(供外部调用)
    func ensureScrollMonitorInstalled() {
        guard let window = window else {
            // print("⚠️ [CAGrid #\(instanceId)] ensureScrollMonitorInstalled: no window")
            return
        }

        // 只要有窗口且没有监听器就安装(可见性在事件处理时检查)
        if scrollEventMonitor == nil {
            // print("🔄 [CAGrid #\(instanceId)] ensureScrollMonitorInstalled: monitor missing, installing")
            setupScrollEventMonitor()
            makeFirstResponderIfAvailable()
        }
    }

    /// 获取实例ID(用于调试)
    var debugInstanceId: Int { instanceId }
}

```

## /LaunchNext/Extensions.swift

```swift path="/LaunchNext/Extensions.swift" 
import SwiftUI
import AppKit

// MARK: - Color Extensions
extension Color {
    static var launchpadBorder: Color {
        Color(.systemBlue)
    }
}

// MARK: - Font Extensions
extension Font {
    static var `default`: Font {
        .system(size: 11, weight: .medium)
    }
}

// MARK: - View Extensions for Glass Effect
extension View {
    @ViewBuilder
    func liquidGlass<S: Shape>(in shape: S, isEnabled: Bool = true) -> some View {
        if #available(macOS 26.0, iOS 18.0, *) {
            self.glassEffect(.regular, in: shape)
        } else {
            self.background(.ultraThinMaterial, in: shape)
        }
    }

    @ViewBuilder
    func liquidGlass(isEnabled: Bool = true) -> some View {
        if #available(macOS 26.0, iOS 18.0, *) {
            self.glassEffect(.regular)
        } else {
            self.background(.ultraThinMaterial)
        }
    }
}
 

```

## /LaunchNext/Gesture/GestureConfiguration.swift

```swift path="/LaunchNext/Gesture/GestureConfiguration.swift" 
import Foundation

struct GestureConfiguration: Equatable {
    var isEnabled: Bool
    var closeOnPinchOutEnabled: Bool = false
    var tapEnabled: Bool = false
    var tapTogglesWindow: Bool = false
    var deviceSelectionMode: GestureDeviceSelectionMode = .automatic
    var selectedDeviceIDs: [String] = []
    var requiredFingerCount: Int = 4
    var stableContactDuration: TimeInterval = 0.03
    var openTriggerScaleRatio: Double = 0.84
    var closeTriggerScaleRatio: Double = 1.06
    var openPerFingerRadiusRatio: Double = 0.96
    var closeLeadingFingerRadiusRatio: Double = 1.12
    var minimumOpenParticipatingFingerCount: Int = 3
    var minimumCloseLeadingGap: Double = 0.06
    var maximumCloseSupportingSpread: Double = 0.22
    var requiredConsecutiveMatches: Int = 2
    var cooldownDuration: TimeInterval = 0.85
    var maximumCentroidDriftRatio: Double = 0.55
    var minimumBaselineScale: Double = 0.10
    var tapMaxDuration: TimeInterval = 0.20
    var tapMaxFingerMovement: Double = 0.045
    var tapMaxScaleDeviation: Double = 0.10
}

```

## /LaunchNext/Gesture/GestureInputDevice.swift

```swift path="/LaunchNext/Gesture/GestureInputDevice.swift" 
import Foundation

enum GestureDeviceSelectionMode: String, CaseIterable, Codable, Identifiable {
    case automatic
    case selected

    var id: String { rawValue }
}

struct GestureInputDevice: Identifiable, Hashable {
    let id: String
    let name: String
    let isBuiltIn: Bool
    let kind: GestureInputDeviceKind
    let familyID: Int
    let sensorSurfaceWidth: Int
    let sensorSurfaceHeight: Int
    let isRecommended: Bool
}

enum GestureInputDeviceKind: String, Hashable {
    case trackpad
    case mouse
    case touchBar
    case unknown
}

extension OMSDeviceInfo {
    var isGestureTrackpadCandidate: Bool {
        isTrackpad || deviceName.localizedCaseInsensitiveContains("Trackpad")
    }

    var isRecommendedGestureDevice: Bool {
        isGestureTrackpadCandidate && gestureInputDeviceKind == .trackpad
    }

    var gestureInputDeviceKind: GestureInputDeviceKind {
        if deviceName.localizedCaseInsensitiveContains("Touch Bar") {
            return .touchBar
        }
        if deviceName.localizedCaseInsensitiveContains("Mouse") {
            return .mouse
        }
        if isGestureTrackpadCandidate {
            return .trackpad
        }
        return .unknown
    }
}

```

## /LaunchNext/ThirdParty/OpenMultitouchSupport/Framework/OpenMultitouchSupportXCF/OpenMTListener.h

```h path="/LaunchNext/ThirdParty/OpenMultitouchSupport/Framework/OpenMultitouchSupportXCF/OpenMTListener.h" 
//
//  OpenMTListener.h
//  OpenMultitouchSupport
//
//  Created by Takuto Nakamura on 2019/07/11.
//  Copyright © 2019 Takuto Nakamura. All rights reserved.
//

#ifndef OpenMTListener_h
#define OpenMTListener_h

#import <Foundation/Foundation.h>

@interface OpenMTListener: NSObject

@property (assign, readwrite) BOOL listening;

@end

#endif /* OpenMTListener_h */

```


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!