@burdenoff/microfe-hookmovies 2026.705.2 → 2026.706.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/hookmovies/HookMoviesRoutes.js +26 -6
  2. package/dist/hookmovies/HookMoviesRoutes.js.map +1 -1
  3. package/dist/hookmovies/components/index.js +7 -0
  4. package/dist/hookmovies/hooks/index.js +1 -0
  5. package/dist/hookmovies/hooks/useFiles.js +97 -0
  6. package/dist/hookmovies/hooks/useFiles.js.map +1 -0
  7. package/dist/hookmovies/hooks/useHookMoviesApi.js +204 -52
  8. package/dist/hookmovies/hooks/useHookMoviesApi.js.map +1 -1
  9. package/dist/hookmovies/pages/HomePage.js +2 -2
  10. package/dist/hookmovies/pages/HomePage.js.map +1 -1
  11. package/dist/hookmovies/pages/auditions/AuditionDetailPage.js +100 -45
  12. package/dist/hookmovies/pages/auditions/AuditionDetailPage.js.map +1 -1
  13. package/dist/hookmovies/pages/catalog/TitleDetailPage.js +1340 -266
  14. package/dist/hookmovies/pages/catalog/TitleDetailPage.js.map +1 -1
  15. package/dist/hookmovies/pages/catalog/TitlesPage.js +95 -74
  16. package/dist/hookmovies/pages/catalog/TitlesPage.js.map +1 -1
  17. package/dist/hookmovies/pages/consumer/DiscoveryPage.js +422 -0
  18. package/dist/hookmovies/pages/consumer/DiscoveryPage.js.map +1 -0
  19. package/dist/hookmovies/pages/consumer/HooksPage.js +252 -0
  20. package/dist/hookmovies/pages/consumer/HooksPage.js.map +1 -0
  21. package/dist/hookmovies/pages/consumer/PlayerPage.js +542 -0
  22. package/dist/hookmovies/pages/consumer/PlayerPage.js.map +1 -0
  23. package/dist/hookmovies/pages/consumer/TitleViewPage.js +424 -0
  24. package/dist/hookmovies/pages/consumer/TitleViewPage.js.map +1 -0
  25. package/dist/hookmovies/pages/distribution/RightsWindowsPage.js +1 -1
  26. package/dist/hookmovies/pages/distribution/RightsWindowsPage.js.map +1 -1
  27. package/dist/hookmovies/pages/library/EntitlementsPage.js +273 -75
  28. package/dist/hookmovies/pages/library/EntitlementsPage.js.map +1 -1
  29. package/dist/hookmovies/pages/library/ListsPage.js +186 -106
  30. package/dist/hookmovies/pages/library/ListsPage.js.map +1 -1
  31. package/package.json +1 -1
@@ -0,0 +1,252 @@
1
+ import { useHookMovies as e } from "../../context/HookMoviesContext.js";
2
+ import { cn as t } from "../../utils/cn.js";
3
+ import { useClips as n } from "../../hooks/useHookMoviesApi.js";
4
+ import { useFileUrl as r } from "../../hooks/useFiles.js";
5
+ import { ErrorState as i, LoadingState as a } from "../../components/StateViews.js";
6
+ import { EmptyState as o } from "../../components/EmptyState.js";
7
+ import "../../components/index.js";
8
+ import { useCallback as s, useEffect as c, useRef as l, useState as u } from "react";
9
+ import { ArrowLeft as d, Film as f, Heart as p, Loader2 as m, MessageCircle as h, Volume2 as g, VolumeX as _ } from "lucide-react";
10
+ import { jsx as v, jsxs as y } from "react/jsx-runtime";
11
+ import { GlassCard as b } from "@burdenoff/fe-libs/ui";
12
+ //#region src/hookmovies/pages/consumer/HooksPage.tsx
13
+ var x = 30, S = .6, C = "rounded-full border border-border-seam bg-bg-surface/70 backdrop-blur-md text-text-secondary", w = "flex h-12 w-12 items-center justify-center rounded-full border border-border-seam bg-bg-surface/70 text-text-primary backdrop-blur-md transition-colors hover:bg-bg-surface hover:border-border-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus", T = ({ clip: e, unmuted: n, onToggleSound: i, onOpenTitle: a }) => {
14
+ let o = l(null), d = l(null), [x, T] = u(!1), [E, D] = u(0), [O, k] = u(!1), { data: A, isLoading: j } = r(e.videoFileId), { data: M } = r(e.posterFileId);
15
+ c(() => {
16
+ let e = o.current;
17
+ if (!e) return;
18
+ let t = new IntersectionObserver((e) => {
19
+ let t = e[0];
20
+ t && T(t.isIntersecting && t.intersectionRatio >= S);
21
+ }, { threshold: [
22
+ 0,
23
+ S,
24
+ 1
25
+ ] });
26
+ return t.observe(e), () => t.disconnect();
27
+ }, []), c(() => {
28
+ let e = d.current;
29
+ if (e) if (x && A) {
30
+ let t = e.play();
31
+ t && t.catch(() => {});
32
+ } else e.pause();
33
+ }, [x, A]), c(() => {
34
+ let e = d.current;
35
+ e && (e.muted = !n || !x);
36
+ }, [n, x]);
37
+ let N = s(() => {
38
+ let e = d.current;
39
+ !e || !Number.isFinite(e.duration) || e.duration === 0 || D(e.currentTime / e.duration);
40
+ }, []), P = e.caption?.trim() || "Untitled clip";
41
+ return /* @__PURE__ */ y("section", {
42
+ ref: o,
43
+ "aria-roledescription": "clip",
44
+ "aria-label": P,
45
+ className: "relative h-full w-full shrink-0 snap-start snap-always overflow-hidden bg-bg-sunken",
46
+ children: [
47
+ /* @__PURE__ */ y("div", {
48
+ role: "presentation",
49
+ onClick: i,
50
+ className: "absolute inset-0 h-full w-full cursor-pointer",
51
+ children: [/* @__PURE__ */ v("video", {
52
+ ref: d,
53
+ className: "h-full w-full object-cover",
54
+ src: A ?? void 0,
55
+ poster: M ?? void 0,
56
+ loop: !0,
57
+ muted: !0,
58
+ playsInline: !0,
59
+ preload: "metadata",
60
+ "aria-label": P,
61
+ onTimeUpdate: N
62
+ }), j && !A && !M && /* @__PURE__ */ y("div", {
63
+ className: "absolute inset-0 flex items-center justify-center",
64
+ children: [/* @__PURE__ */ v(m, {
65
+ className: "h-8 w-8 animate-spin text-text-secondary",
66
+ "aria-hidden": "true"
67
+ }), /* @__PURE__ */ v("span", {
68
+ className: "sr-only",
69
+ children: "Loading clip…"
70
+ })]
71
+ })]
72
+ }),
73
+ /* @__PURE__ */ v("div", {
74
+ "aria-hidden": "true",
75
+ className: "pointer-events-none absolute inset-x-0 bottom-0 h-2/5 bg-gradient-to-t from-bg-overlay to-transparent"
76
+ }),
77
+ x && !n && /* @__PURE__ */ v("div", {
78
+ className: "pointer-events-none absolute left-1/2 top-4 -translate-x-1/2",
79
+ children: /* @__PURE__ */ y("span", {
80
+ className: t(C, "inline-flex items-center gap-1.5 px-3 py-1 text-xs font-medium"),
81
+ children: [/* @__PURE__ */ v(_, {
82
+ className: "h-3.5 w-3.5",
83
+ "aria-hidden": "true"
84
+ }), "Tap for sound"]
85
+ })
86
+ }),
87
+ /* @__PURE__ */ y("div", {
88
+ className: "absolute bottom-28 right-3 z-10 flex flex-col items-center gap-4 sm:bottom-24",
89
+ children: [
90
+ /* @__PURE__ */ y("div", {
91
+ className: "flex flex-col items-center gap-1.5",
92
+ children: [/* @__PURE__ */ v("button", {
93
+ type: "button",
94
+ "aria-pressed": O,
95
+ "aria-label": O ? "Remove like" : "Like this clip",
96
+ onClick: (e) => {
97
+ e.stopPropagation(), k((e) => !e);
98
+ },
99
+ className: w,
100
+ children: /* @__PURE__ */ v(p, {
101
+ className: t("h-5 w-5 transition-transform", O && "scale-110 fill-status-error-text text-status-error-text"),
102
+ "aria-hidden": "true"
103
+ })
104
+ }), /* @__PURE__ */ v("span", {
105
+ className: "text-[0.65rem] font-medium text-text-secondary",
106
+ children: "Like"
107
+ })]
108
+ }),
109
+ /* @__PURE__ */ y("div", {
110
+ className: "flex flex-col items-center gap-1.5",
111
+ children: [/* @__PURE__ */ v("button", {
112
+ type: "button",
113
+ "aria-label": "Read and write reviews for the full title",
114
+ onClick: (t) => {
115
+ t.stopPropagation(), a(e.titleId);
116
+ },
117
+ className: w,
118
+ children: /* @__PURE__ */ v(h, {
119
+ className: "h-5 w-5",
120
+ "aria-hidden": "true"
121
+ })
122
+ }), /* @__PURE__ */ v("span", {
123
+ className: "text-[0.65rem] font-medium text-text-secondary",
124
+ children: "Reviews"
125
+ })]
126
+ }),
127
+ /* @__PURE__ */ y("div", {
128
+ className: "flex flex-col items-center gap-1.5",
129
+ children: [/* @__PURE__ */ v("button", {
130
+ type: "button",
131
+ "aria-pressed": n,
132
+ "aria-label": n ? "Mute" : "Unmute",
133
+ onClick: (e) => {
134
+ e.stopPropagation(), i();
135
+ },
136
+ className: w,
137
+ children: v(n ? g : _, {
138
+ className: "h-5 w-5",
139
+ "aria-hidden": "true"
140
+ })
141
+ }), /* @__PURE__ */ v("span", {
142
+ className: "text-[0.65rem] font-medium text-text-secondary",
143
+ children: n ? "Sound" : "Muted"
144
+ })]
145
+ })
146
+ ]
147
+ }),
148
+ /* @__PURE__ */ v("div", {
149
+ className: "absolute inset-x-0 bottom-6 z-10 px-4 pr-20",
150
+ children: /* @__PURE__ */ y(b, {
151
+ treatment: "glass",
152
+ className: "border-border-seam p-4",
153
+ children: [
154
+ e.language && /* @__PURE__ */ v("span", {
155
+ className: t(C, "mb-2 inline-flex px-2.5 py-0.5 text-[0.7rem] font-medium"),
156
+ children: e.language.toUpperCase()
157
+ }),
158
+ /* @__PURE__ */ v("p", {
159
+ className: "line-clamp-2 text-sm font-medium text-text-primary",
160
+ children: P
161
+ }),
162
+ /* @__PURE__ */ y("button", {
163
+ type: "button",
164
+ onClick: (t) => {
165
+ t.stopPropagation(), a(e.titleId);
166
+ },
167
+ className: t("mt-3 inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-semibold", "bg-action-primary-bg text-action-primary-text transition-opacity hover:opacity-90", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus"),
168
+ children: [/* @__PURE__ */ v(f, {
169
+ className: "h-4 w-4",
170
+ "aria-hidden": "true"
171
+ }), "Watch full film"]
172
+ })
173
+ ]
174
+ })
175
+ }),
176
+ /* @__PURE__ */ v("div", {
177
+ className: "absolute inset-x-0 bottom-0 z-20 h-1 bg-bg-overlay",
178
+ role: "progressbar",
179
+ "aria-label": "Clip playback progress",
180
+ "aria-valuemin": 0,
181
+ "aria-valuemax": 100,
182
+ "aria-valuenow": Math.round(Math.min(E, 1) * 100),
183
+ children: /* @__PURE__ */ v("div", {
184
+ className: "h-full bg-action-primary-bg transition-[width] duration-150 ease-linear",
185
+ style: { width: `${Math.min(E, 1) * 100}%` }
186
+ })
187
+ })
188
+ ]
189
+ });
190
+ }, E = () => {
191
+ let { navigateTo: r } = e(), { data: c, isLoading: l, isError: f, refetch: p } = n(void 0, void 0, {
192
+ limit: x,
193
+ offset: 0
194
+ }), [m, h] = u(!1), g = s(() => h((e) => !e), []), _ = s((e) => r(`title/${e}`), [r]), b = c?.items ?? [];
195
+ return /* @__PURE__ */ v("div", {
196
+ className: "h-[100dvh] w-full overflow-hidden bg-bg-canvas",
197
+ children: l ? /* @__PURE__ */ v(a, {
198
+ label: "Loading Hooks…",
199
+ className: "min-h-[100dvh]"
200
+ }) : f ? /* @__PURE__ */ v(i, {
201
+ title: "Couldn't load Hooks",
202
+ message: "We couldn't reach the clip feed. Check your connection and try again.",
203
+ onRetry: () => void p(),
204
+ className: "min-h-[100dvh]"
205
+ }) : b.length === 0 ? /* @__PURE__ */ v("div", {
206
+ className: "flex min-h-[100dvh] items-center justify-center px-6",
207
+ children: /* @__PURE__ */ v(o, {
208
+ illustration: "empty-media",
209
+ title: "No Hooks yet",
210
+ description: "Short-form clips promoting titles will appear here as soon as they're published.",
211
+ action: /* @__PURE__ */ v("button", {
212
+ type: "button",
213
+ onClick: () => r("browse"),
214
+ className: "inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90",
215
+ children: "Browse titles"
216
+ })
217
+ })
218
+ }) : /* @__PURE__ */ y("div", {
219
+ className: "relative mx-auto h-full w-full max-w-[460px]",
220
+ children: [/* @__PURE__ */ y("div", {
221
+ className: "pointer-events-none absolute inset-x-0 top-0 z-30 flex items-center justify-between gap-2 p-3",
222
+ children: [/* @__PURE__ */ v("button", {
223
+ type: "button",
224
+ "aria-label": "Back to browse",
225
+ onClick: () => r("browse"),
226
+ className: t(w, "pointer-events-auto h-10 w-10"),
227
+ children: /* @__PURE__ */ v(d, {
228
+ className: "h-5 w-5",
229
+ "aria-hidden": "true"
230
+ })
231
+ }), /* @__PURE__ */ v("span", {
232
+ className: t(C, "px-3 py-1 text-xs font-semibold uppercase tracking-wide text-text-primary"),
233
+ children: "Hooks"
234
+ })]
235
+ }), /* @__PURE__ */ v("div", {
236
+ role: "feed",
237
+ "aria-label": "Short-form clips",
238
+ className: t("h-full w-full overflow-y-auto overflow-x-hidden snap-y snap-mandatory scroll-smooth", "[scrollbar-width:none] [&::-webkit-scrollbar]:hidden"),
239
+ children: b.map((e) => /* @__PURE__ */ v(T, {
240
+ clip: e,
241
+ unmuted: m,
242
+ onToggleSound: g,
243
+ onOpenTitle: _
244
+ }, e.id))
245
+ })]
246
+ })
247
+ });
248
+ };
249
+ //#endregion
250
+ export { E as HooksPage };
251
+
252
+ //# sourceMappingURL=HooksPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HooksPage.js","names":[],"sources":["../../../../src/hookmovies/pages/consumer/HooksPage.tsx"],"sourcesContent":["/**\n * Hooks — short-form vertical clip feed\n * @module hookmovies/pages/consumer\n *\n * A full-viewport, snap-scrolling vertical feed of short-form clips (the\n * `hokListClips` catalog) — HookMovies' \"Hooks\". Each slide autoplays a single\n * muted, looping clip while it is in view (driven by an IntersectionObserver);\n * every other slide is paused so only one video ever plays at a time. Tap a\n * clip to toggle sound; scroll to advance.\n *\n * Media is never stored here — the `<video>` `src`/`poster` resolve to\n * short-lived presigned URLs via {@link useFileUrl} against wspace-files-svc.\n * Each slide overlays the caption, language, a like/review action rail, a\n * \"Watch full film\" link into the parent title, and a thin playback progress\n * line.\n *\n * Styling stays on fe-libs semantic tokens only (no raw colors); the immersive\n * surface floats surface-colored glass chips over the media so overlay text\n * keeps contrast in both light and dark themes.\n */\n\nimport { type FC, useCallback, useEffect, useRef, useState } from 'react';\nimport { ArrowLeft, Film, Heart, Loader2, MessageCircle, Volume2, VolumeX } from 'lucide-react';\n\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { EmptyState, ErrorState, LoadingState } from '../../components';\nimport { useHookMovies } from '../../context/HookMoviesContext';\nimport { useClips, type HokClip } from '../../hooks/useHookMoviesApi';\nimport { useFileUrl } from '../../hooks/useFiles';\nimport { cn } from '../../utils/cn';\n\n/** How many recent clips to pull into the feed. */\nconst FEED_PAGE_SIZE = 30;\n\n/** Fraction of a slide that must be visible before its clip becomes \"active\". */\nconst ACTIVE_VISIBILITY_RATIO = 0.6;\n\n/** Shared floating-chip look (surface glass) — legible over media in both themes. */\nconst CHIP_CLASS =\n 'rounded-full border border-border-seam bg-bg-surface/70 backdrop-blur-md text-text-secondary';\n\n/** Shared circular action-rail button. */\nconst RAIL_BUTTON_CLASS =\n 'flex h-12 w-12 items-center justify-center rounded-full border border-border-seam ' +\n 'bg-bg-surface/70 text-text-primary backdrop-blur-md transition-colors ' +\n 'hover:bg-bg-surface hover:border-border-strong ' +\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus';\n\ninterface ClipSlideProps {\n clip: HokClip;\n /** Global sound state — when true, the active slide plays with audio. */\n unmuted: boolean;\n onToggleSound: () => void;\n onOpenTitle: (titleId: string) => void;\n}\n\n/**\n * A single full-height clip in the feed. Owns its own video element, resolves\n * its presigned media URLs, and plays/pauses itself based on whether it is the\n * in-view slide (via an IntersectionObserver on its container).\n */\nconst ClipSlide: FC<ClipSlideProps> = ({ clip, unmuted, onToggleSound, onOpenTitle }) => {\n const containerRef = useRef<HTMLElement | null>(null);\n const videoRef = useRef<HTMLVideoElement | null>(null);\n\n const [isActive, setIsActive] = useState(false);\n const [progress, setProgress] = useState(0);\n const [liked, setLiked] = useState(false);\n\n const { data: videoUrl, isLoading: videoLoading } = useFileUrl(clip.videoFileId);\n const { data: posterUrl } = useFileUrl(clip.posterFileId);\n\n // Become active only when a healthy majority of the slide is on screen.\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n const observer = new IntersectionObserver(\n (entries) => {\n const entry = entries[0];\n if (entry) {\n setIsActive(entry.isIntersecting && entry.intersectionRatio >= ACTIVE_VISIBILITY_RATIO);\n }\n },\n { threshold: [0, ACTIVE_VISIBILITY_RATIO, 1] }\n );\n observer.observe(el);\n return () => observer.disconnect();\n }, []);\n\n // Play the active slide; pause (and rewind) everything else so exactly one\n // clip is ever playing.\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n if (isActive && videoUrl) {\n const played = video.play();\n if (played) {\n played.catch(() => {\n /* Autoplay can be rejected until the user interacts — safe to ignore. */\n });\n }\n } else {\n video.pause();\n }\n }, [isActive, videoUrl]);\n\n // React does not reliably reflect the `muted` prop onto the element, so drive\n // it imperatively. Only the active slide may carry sound.\n useEffect(() => {\n const video = videoRef.current;\n if (video) video.muted = !unmuted || !isActive;\n }, [unmuted, isActive]);\n\n const handleTimeUpdate = useCallback(() => {\n const video = videoRef.current;\n if (!video || !Number.isFinite(video.duration) || video.duration === 0) return;\n setProgress(video.currentTime / video.duration);\n }, []);\n\n const captionLabel = clip.caption?.trim() || 'Untitled clip';\n\n return (\n <section\n ref={containerRef}\n aria-roledescription=\"clip\"\n aria-label={captionLabel}\n className=\"relative h-full w-full shrink-0 snap-start snap-always overflow-hidden bg-bg-sunken\"\n >\n {/* Media — tap toggles sound. A dedicated rail button provides the same\n action for keyboard users. */}\n <div\n role=\"presentation\"\n onClick={onToggleSound}\n className=\"absolute inset-0 h-full w-full cursor-pointer\"\n >\n <video\n ref={videoRef}\n className=\"h-full w-full object-cover\"\n src={videoUrl ?? undefined}\n poster={posterUrl ?? undefined}\n loop\n muted\n playsInline\n preload=\"metadata\"\n aria-label={captionLabel}\n onTimeUpdate={handleTimeUpdate}\n />\n\n {/* URL still resolving and nothing to show yet. */}\n {videoLoading && !videoUrl && !posterUrl && (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"sr-only\">Loading clip…</span>\n </div>\n )}\n </div>\n\n {/* Legibility scrim behind the bottom overlay (decorative). */}\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-x-0 bottom-0 h-2/5 bg-gradient-to-t from-bg-overlay to-transparent\"\n />\n\n {/* Sound-off hint chip while muted + active. */}\n {isActive && !unmuted && (\n <div className=\"pointer-events-none absolute left-1/2 top-4 -translate-x-1/2\">\n <span\n className={cn(\n CHIP_CLASS,\n 'inline-flex items-center gap-1.5 px-3 py-1 text-xs font-medium'\n )}\n >\n <VolumeX className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n Tap for sound\n </span>\n </div>\n )}\n\n {/* Right-hand action rail: like, reviews, sound. */}\n <div className=\"absolute bottom-28 right-3 z-10 flex flex-col items-center gap-4 sm:bottom-24\">\n <div className=\"flex flex-col items-center gap-1.5\">\n <button\n type=\"button\"\n aria-pressed={liked}\n aria-label={liked ? 'Remove like' : 'Like this clip'}\n onClick={(event) => {\n event.stopPropagation();\n setLiked((value) => !value);\n }}\n className={RAIL_BUTTON_CLASS}\n >\n <Heart\n className={cn(\n 'h-5 w-5 transition-transform',\n liked && 'scale-110 fill-status-error-text text-status-error-text'\n )}\n aria-hidden=\"true\"\n />\n </button>\n <span className=\"text-[0.65rem] font-medium text-text-secondary\">Like</span>\n </div>\n\n <div className=\"flex flex-col items-center gap-1.5\">\n <button\n type=\"button\"\n aria-label=\"Read and write reviews for the full title\"\n onClick={(event) => {\n event.stopPropagation();\n onOpenTitle(clip.titleId);\n }}\n className={RAIL_BUTTON_CLASS}\n >\n <MessageCircle className=\"h-5 w-5\" aria-hidden=\"true\" />\n </button>\n <span className=\"text-[0.65rem] font-medium text-text-secondary\">Reviews</span>\n </div>\n\n <div className=\"flex flex-col items-center gap-1.5\">\n <button\n type=\"button\"\n aria-pressed={unmuted}\n aria-label={unmuted ? 'Mute' : 'Unmute'}\n onClick={(event) => {\n event.stopPropagation();\n onToggleSound();\n }}\n className={RAIL_BUTTON_CLASS}\n >\n {unmuted ? (\n <Volume2 className=\"h-5 w-5\" aria-hidden=\"true\" />\n ) : (\n <VolumeX className=\"h-5 w-5\" aria-hidden=\"true\" />\n )}\n </button>\n <span className=\"text-[0.65rem] font-medium text-text-secondary\">\n {unmuted ? 'Sound' : 'Muted'}\n </span>\n </div>\n </div>\n\n {/* Bottom overlay: language, caption, and the full-film link. */}\n <div className=\"absolute inset-x-0 bottom-6 z-10 px-4 pr-20\">\n <GlassCard treatment=\"glass\" className=\"border-border-seam p-4\">\n {clip.language && (\n <span\n className={cn(CHIP_CLASS, 'mb-2 inline-flex px-2.5 py-0.5 text-[0.7rem] font-medium')}\n >\n {clip.language.toUpperCase()}\n </span>\n )}\n <p className=\"line-clamp-2 text-sm font-medium text-text-primary\">{captionLabel}</p>\n <button\n type=\"button\"\n onClick={(event) => {\n event.stopPropagation();\n onOpenTitle(clip.titleId);\n }}\n className={cn(\n 'mt-3 inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-semibold',\n 'bg-action-primary-bg text-action-primary-text transition-opacity hover:opacity-90',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus'\n )}\n >\n <Film className=\"h-4 w-4\" aria-hidden=\"true\" />\n Watch full film\n </button>\n </GlassCard>\n </div>\n\n {/* Thin playback progress line. */}\n <div\n className=\"absolute inset-x-0 bottom-0 z-20 h-1 bg-bg-overlay\"\n role=\"progressbar\"\n aria-label=\"Clip playback progress\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(Math.min(progress, 1) * 100)}\n >\n <div\n className=\"h-full bg-action-primary-bg transition-[width] duration-150 ease-linear\"\n style={{ width: `${Math.min(progress, 1) * 100}%` }}\n />\n </div>\n </section>\n );\n};\n\n/**\n * The Hooks feed page. Loads recent clips across all titles and renders them as\n * a vertical, snap-scrolling short-form feed.\n */\nexport const HooksPage: FC = () => {\n const { navigateTo } = useHookMovies();\n const { data, isLoading, isError, refetch } = useClips(undefined, undefined, {\n limit: FEED_PAGE_SIZE,\n offset: 0,\n });\n\n const [unmuted, setUnmuted] = useState(false);\n const toggleSound = useCallback(() => setUnmuted((value) => !value), []);\n const openTitle = useCallback((titleId: string) => navigateTo(`title/${titleId}`), [navigateTo]);\n\n const clips = data?.items ?? [];\n\n const renderBody = () => {\n if (isLoading) {\n return <LoadingState label=\"Loading Hooks…\" className=\"min-h-[100dvh]\" />;\n }\n if (isError) {\n return (\n <ErrorState\n title=\"Couldn't load Hooks\"\n message=\"We couldn't reach the clip feed. Check your connection and try again.\"\n onRetry={() => void refetch()}\n className=\"min-h-[100dvh]\"\n />\n );\n }\n if (clips.length === 0) {\n return (\n <div className=\"flex min-h-[100dvh] items-center justify-center px-6\">\n <EmptyState\n illustration=\"empty-media\"\n title=\"No Hooks yet\"\n description=\"Short-form clips promoting titles will appear here as soon as they're published.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('browse')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90\"\n >\n Browse titles\n </button>\n }\n />\n </div>\n );\n }\n\n return (\n <div className=\"relative mx-auto h-full w-full max-w-[460px]\">\n {/* Fixed header over the feed column. */}\n <div className=\"pointer-events-none absolute inset-x-0 top-0 z-30 flex items-center justify-between gap-2 p-3\">\n <button\n type=\"button\"\n aria-label=\"Back to browse\"\n onClick={() => navigateTo('browse')}\n className={cn(RAIL_BUTTON_CLASS, 'pointer-events-auto h-10 w-10')}\n >\n <ArrowLeft className=\"h-5 w-5\" aria-hidden=\"true\" />\n </button>\n <span\n className={cn(\n CHIP_CLASS,\n 'px-3 py-1 text-xs font-semibold uppercase tracking-wide text-text-primary'\n )}\n >\n Hooks\n </span>\n </div>\n\n <div\n role=\"feed\"\n aria-label=\"Short-form clips\"\n className={cn(\n 'h-full w-full overflow-y-auto overflow-x-hidden snap-y snap-mandatory scroll-smooth',\n '[scrollbar-width:none] [&::-webkit-scrollbar]:hidden'\n )}\n >\n {clips.map((clip) => (\n <ClipSlide\n key={clip.id}\n clip={clip}\n unmuted={unmuted}\n onToggleSound={toggleSound}\n onOpenTitle={openTitle}\n />\n ))}\n </div>\n </div>\n );\n };\n\n return <div className=\"h-[100dvh] w-full overflow-hidden bg-bg-canvas\">{renderBody()}</div>;\n};\n"],"mappings":"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"}