diff --git a/app/pages/org/[org].stories.ts b/app/pages/org/[org].stories.ts
new file mode 100644
index 0000000000..0911a6e83c
--- /dev/null
+++ b/app/pages/org/[org].stories.ts
@@ -0,0 +1,137 @@
+import Org from './[org].vue'
+import type { Meta, StoryObj } from '@storybook-vue/nuxt'
+import { pageDecorator } from '../../../.storybook/decorators'
+import {
+ mockOrgPackagesSuccess,
+ mockOrgPackagesSingle,
+ mockOrgPackagesEmpty,
+ mockOrgPackagesNotFound,
+ mockOrgPackagesLoading,
+} from '../../storybook/mocks/handlers/registry-org'
+
+const meta: Meta = {
+ component: Org,
+ parameters: {
+ layout: 'fullscreen',
+ },
+ decorators: [pageDecorator],
+}
+
+export default meta
+type Story = StoryObj
+
+/**
+ * Default org page showing the @npmx organization with multiple packages.
+ * Displays package list with filtering, sorting, and view mode controls.
+ * The MSW handler mocks both the org packages endpoint and Algolia search.
+ */
+export const Default: Story = {
+ parameters: {
+ msw: { handlers: mockOrgPackagesSuccess },
+ },
+ render: () => ({
+ components: { Org },
+ setup() {
+ const isReady = ref(false)
+ useRouter()
+ .replace('/org/npmx')
+ .then(() => {
+ isReady.value = true
+ })
+ return { isReady }
+ },
+ template: '',
+ }),
+}
+
+/**
+ * Organization with only a single package.
+ * Shows the org page layout with minimal content.
+ */
+export const SinglePackage: Story = {
+ parameters: {
+ msw: { handlers: mockOrgPackagesSingle },
+ },
+ render: () => ({
+ components: { Org },
+ setup() {
+ const isReady = ref(false)
+ useRouter()
+ .replace('/org/single-org')
+ .then(() => {
+ isReady.value = true
+ })
+ return { isReady }
+ },
+ template: '',
+ }),
+}
+
+/**
+ * Empty organization with zero packages.
+ * Shows the "This organization has no packages" message.
+ */
+export const EmptyOrg: Story = {
+ parameters: {
+ msw: { handlers: mockOrgPackagesEmpty },
+ },
+ render: () => ({
+ components: { Org },
+ setup() {
+ const isReady = ref(false)
+ useRouter()
+ .replace('/org/empty-org')
+ .then(() => {
+ isReady.value = true
+ })
+ return { isReady }
+ },
+ template: '',
+ }),
+}
+
+/**
+ * Organization not found (404 error).
+ * The org endpoint returns a 404 error and the page displays an error state.
+ */
+export const NotFound: Story = {
+ parameters: {
+ msw: { handlers: mockOrgPackagesNotFound },
+ },
+ render: () => ({
+ components: { Org },
+ setup() {
+ const isReady = ref(false)
+ useRouter()
+ .replace('/org/nonexistent-org')
+ .then(() => {
+ isReady.value = true
+ })
+ return { isReady }
+ },
+ template: '',
+ }),
+}
+
+/**
+ * Loading state when the API request is pending.
+ * MSW handlers delay responses indefinitely to show the loading spinner.
+ */
+export const Loading: Story = {
+ parameters: {
+ msw: { handlers: mockOrgPackagesLoading },
+ },
+ render: () => ({
+ components: { Org },
+ setup() {
+ const isReady = ref(false)
+ useRouter()
+ .replace('/org/npmx')
+ .then(() => {
+ isReady.value = true
+ })
+ return { isReady }
+ },
+ template: '',
+ }),
+}
diff --git a/app/storybook/mocks/handlers/registry-org.ts b/app/storybook/mocks/handlers/registry-org.ts
new file mode 100644
index 0000000000..f7c841958a
--- /dev/null
+++ b/app/storybook/mocks/handlers/registry-org.ts
@@ -0,0 +1,166 @@
+import { http, HttpResponse } from 'msw'
+
+/**
+ * Helper to create mock AlgoliaHit objects (mimics Algolia API response format)
+ */
+function createMockAlgoliaHit(
+ name: string,
+ overrides: {
+ description?: string
+ version?: string
+ downloadsLast30Days?: number
+ keywords?: string[]
+ modified?: number
+ license?: string
+ } = {},
+) {
+ return {
+ objectID: name,
+ name,
+ version: overrides.version || '1.2.3',
+ description: overrides.description || `Mock package ${name}`,
+ modified: overrides.modified || new Date('2026-01-22T10:07:07.000Z').getTime(),
+ homepage: `https://github.com/org/${name.replace('@', '').replace('/', '-')}`,
+ repository: {
+ url: `https://github.com/org/${name.replace('@', '').replace('/', '-')}`,
+ type: 'git',
+ },
+ owners: [
+ {
+ name: 'Patak Dog',
+ email: 'patak@patak.dog',
+ },
+ ],
+ downloadsLast30Days: overrides.downloadsLast30Days || 100000,
+ downloadsRatio: 1,
+ popular: (overrides.downloadsLast30Days || 100000) > 50000,
+ keywords: overrides.keywords || [],
+ deprecated: false,
+ isDeprecated: false,
+ license: overrides.license || 'MIT',
+ isSecurityHeld: false,
+ }
+}
+
+/**
+ * Mock handler: Org with multiple packages (default success scenario)
+ */
+export const mockOrgPackagesSuccess = [
+ // Return the org package list
+ http.get('/api/registry/org/:org/packages', ({ params }) => {
+ const org = params.org as string
+ const packages = [
+ `@${org}/xmpn`,
+ `@${org}/schema`,
+ `@${org}/i18n`,
+ `@${org}/noodle`,
+ `@${org}/tester`,
+ `${org}`,
+ ]
+
+ return HttpResponse.json({
+ packages,
+ count: packages.length,
+ })
+ }),
+
+ // Mock Algolia getObjects endpoint for package metadata
+ http.post('https://*.algolia.net/1/indexes/*/objects', async ({ request }) => {
+ const body = (await request.json()) as any
+ const requests = body?.requests || []
+
+ // Return AlgoliaHit objects for each requested package
+ const results = requests.map((req: any) => {
+ const packageName = req.objectID
+ const orgMatch = packageName.match(/@([\w-]+)\//) || [packageName, packageName]
+ const org = orgMatch[1]
+ const packageShortName = packageName.replace(`@${org}/`, '').replace(org, '')
+
+ return createMockAlgoliaHit(packageName, {
+ description: `${org.charAt(0).toUpperCase() + org.slice(1)} ${packageShortName} - mocked package`,
+ downloadsLast30Days: 88477,
+ keywords: [org, packageShortName],
+ modified: new Date('2026-01-22T10:07:07.000Z').getTime(),
+ })
+ })
+
+ return HttpResponse.json({ results })
+ }),
+]
+
+/**
+ * Mock handler: Org with single package
+ */
+export const mockOrgPackagesSingle = [
+ http.get('/api/registry/org/:org/packages', ({ params }) => {
+ const org = params.org as string
+ const packageName = `@${org}/only-package`
+
+ return HttpResponse.json({
+ packages: [packageName],
+ count: 1,
+ })
+ }),
+
+ http.post('https://*.algolia.net/1/indexes/*/objects', async ({ request }) => {
+ const body = (await request.json()) as any
+ const requests = body?.requests || []
+
+ // Return AlgoliaHit objects for each requested package
+ const results = requests.map((req: any) => {
+ const packageName = req.objectID
+
+ return createMockAlgoliaHit(packageName, {
+ description: 'The only package in this organization',
+ downloadsLast30Days: 5308, // 1234 weekly
+ keywords: ['single', 'lonely'],
+ modified: new Date('2026-01-22T10:07:07.000Z').getTime(),
+ })
+ })
+
+ return HttpResponse.json({ results })
+ }),
+]
+
+/**
+ * Mock handler: Empty org (no packages)
+ */
+export const mockOrgPackagesEmpty = [
+ http.get('/api/registry/org/:org/packages', () => {
+ return HttpResponse.json({
+ packages: [],
+ count: 0,
+ })
+ }),
+]
+
+/**
+ * Mock handler: Org not found (404 error)
+ */
+export const mockOrgPackagesNotFound = [
+ http.get('/api/registry/org/:org/packages', () => {
+ return HttpResponse.json(
+ {
+ error: 'Not Found',
+ message: 'Organization not found',
+ },
+ { status: 404 },
+ )
+ }),
+]
+
+/**
+ * Mock handler: Loading state (requests never resolve)
+ */
+export const mockOrgPackagesLoading = [
+ http.get('/api/registry/org/:org/packages', async () => {
+ // Delay indefinitely to show loading state
+ await new Promise(() => {})
+ return HttpResponse.json({ packages: [], count: 0 })
+ }),
+ http.post('https://*.algolia.net/1/indexes/*/objects', async () => {
+ // Delay indefinitely to show loading state
+ await new Promise(() => {})
+ return HttpResponse.json({ results: [] })
+ }),
+]