Themed spinner words: Claude is 'Plundering…' then 'Plundered for 1m 3s'. Ten packs (pirate, Shakespeare, corporate, wizard, chef, space, noir, Gen Z, zen…

Claude isn't "Sautéing…" anymore. It's Charting a course…, then Charted a course for 51s. Ten themed word packs for the spinner, or your own words.
✳ Charting a course… (12s · thinking)
● Here's the poem…
✻ Charted a course for 51s · done 3:23 PM
Captured from a real session with the pirate pack. The spinner keeps its word for the whole turn, and the line that closes the turn uses the same word in the past tense.
| Pack | A taste |
|---|---|
🏴☠️ pirate | Plundering · Keelhauling bugs · Counting doubloons |
🎭 shakespeare | Soliloquizing · Perchancing · Consulting the witches |
💼 corporate | Synergizing · Circling back · Boiling the ocean |
🧙 wizard | Conjuring · Polishing the orb · Hexing bugs |
👩🍳 chef | Deglazing · Julienning · Tempering chocolate |
🚀 space | Calibrating thrusters · Engaging hyperdrive · Splashing down |
🕵️ noir | Dusting for prints · Rounding up the usual suspects · Smelling a rat |
✨ genz | Cooking · Locking in · Understanding the assignment |
🍃 zen | Raking the gravel · Sipping tea · Returning to stillness |
💾 retro | Dialing up · Reticulating splines · Downloading more RAM |
random (the default) picks a pack each session.custom option. Write Frobbing|Frobbed to theme the closing line too./spinner <pack> switches mid-turn and remembers the choice. Also /spinner random, /spinner off, and /spinner reset (back to your setting). /spinner lists every pack with samples./plugin marketplace add Singh-AP/awesome-claude-mods
/plugin install spinner-packs@awesome-claude-mods
Requires Claude Code 2.1.287 or later.
| Option | Default | What it does | ||
|---|---|---|---|---|
pack | random | random, one of the ten packs, or off (Claude Code's own words). A /spinner choice wins over this until /spinner reset. | ||
custom | empty | Comma-separated words that replace every pack, e.g. `Frobbing\ | Frobbed, Yak shaving\ | Shaved yaks`. |
themeClosingLine | true | Also theme the line that closes a turn ("Plundered for 1m 3s"). Terminal only. |
| Event | Why |
|---|---|
ui.render on Spinner | Rewrites props.word with next({ ...e, props }), so the engine still draws the glyph, timer and tokens |
ui.render on TurnDuration | Rewrites the past-tense word to match the spinner word that turn showed |
turn.start | Moves to the next word and redraws |
command.run | /spinner |
$.store | Remembers your /spinner choice |
It rewrites props rather than drawing its own tree, so it stacks with other mods on the same sites, such as cost-meter's turn cost.
claude plugin test mods/fun/spinner-packs # 26 tests
hooks/register.ts 130 lines1import type { Register } from 'claude-code'
2
3import { PACK_IDS, PACKS, indexFor, packById, parseCustom, pastAt, pastFor, presentFor, samples, type Pack } from './packs'
4
5// What /spinner chose, read from $.store at session start: a pack id,
6// 'random' or 'off'; undefined follows the `pack` option.
7let chosen: string | undefined
8// The pack 'random' landed on for this session.
9let rolled: Pack | undefined
10// Bumped at every turn start, so each turn gets its own word.
11let turnSeq = 0
12// The pair the spinner last showed, and the pair each closing line took, so
13// "Charting a course…" ends as "Charted a course for 51s" and stays that way.
14let lastIndex: number | undefined
15const closing = new Map<string, number>()
16
17function roll(): Pack {
18 rolled = PACKS[Math.floor(Math.random() * PACKS.length)]!
19 return rolled
20}
21
22export const register: Register = (on, options) => {
23 const custom = parseCustom(String(options.custom ?? ''))
24 const optionPack = String(options.pack ?? 'random')
25 const themeClosingLine = options.themeClosingLine !== false
26
27 const setting = (): string => chosen ?? optionPack
28
29 const currentPack = (): Pack | undefined => {
30 if (custom !== undefined) return custom
31 const id = setting()
32 if (id === 'off') return undefined
33 if (id === 'random') return rolled ?? roll()
34 return packById(id) ?? rolled ?? roll()
35 }
36
37 on('session.start', async ($, e, next) => {
38 const stored = await $.store.get('pack')
39 chosen = typeof stored === 'string' ? stored : undefined
40 await $.command.register({
41 name: 'spinner',
42 description: 'Pick the spinner word pack: /spinner <pack|random|off|reset>',
43 argumentHint: '[pack]',
44 immediate: true,
45 })
46 return next(e)
47 })
48
49 on('turn.start', ($, e, next) => {
50 turnSeq += 1
51 $.ui.invalidate('ui.render')
52 return next(e)
53 })
54
55 // The line that animates while Claude works.
56 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
57 const pack = currentPack()
58 // A state that overrides the word (compacting, retrying) keeps its text.
59 if (pack === undefined || e.props.message !== null) return next(e)
60 // The desktop's row names the step it is on; only its idle "Working" is ours.
61 if (e.surface === 'desktop' && e.props.word !== 'Working') return next(e)
62 lastIndex = indexFor(pack, `${turnSeq}:${e.requestId}`)
63 return next({ ...e, props: { ...e.props, word: pack.words[lastIndex]![0] } })
64 })
65
66 // The line that closes a turn ("Plundered for 1m 3s"), terminal only.
67 on('ui.render', { component: 'TurnDuration' }, async ($, e, next) => {
68 const pack = currentPack()
69 if (pack === undefined || !themeClosingLine) return next(e)
70 let index = closing.get(e.requestId)
71 if (index === undefined && lastIndex !== undefined) {
72 index = lastIndex
73 closing.set(e.requestId, index)
74 if (closing.size > 500) closing.delete(closing.keys().next().value!)
75 }
76 const word = (index !== undefined ? pastAt(pack, index) : undefined) ?? pastFor(pack, e.requestId)
77 if (word === undefined) return next(e)
78 return next({ ...e, props: { ...e.props, word } })
79 })
80
81 on('command.run', { command: 'spinner' }, async ($, e) => {
82 const arg = e.args.trim().toLowerCase()
83
84 if (arg === '' || arg === 'list') {
85 const now = currentPack()
86 const current = custom !== undefined
87 ? 'custom words (from the `custom` option)'
88 : setting() === 'off'
89 ? 'off (Claude Code’s own words)'
90 : setting() === 'random'
91 ? `random, ${now?.emoji ?? ''} ${now?.id ?? ''} this session`
92 : `${now?.emoji ?? ''} ${now?.id ?? ''}`
93 const rows = PACKS.map(p => ` ${p.emoji} ${p.id.padEnd(12)} ${samples(p).join(' · ')}…`)
94 return {
95 text: [
96 `Spinner pack: ${current}.`,
97 '',
98 ...rows,
99 '',
100 'Switch with /spinner <pack>, /spinner random, /spinner off, or /spinner reset (back to your setting).',
101 ].join('\n'),
102 }
103 }
104
105 if (arg === 'reset') {
106 chosen = undefined
107 await $.store.delete('pack')
108 } else if (arg === 'random') {
109 chosen = 'random'
110 roll()
111 await $.store.set('pack', chosen)
112 } else if (arg === 'off') {
113 chosen = 'off'
114 await $.store.set('pack', chosen)
115 } else if (packById(arg) !== undefined) {
116 chosen = arg
117 await $.store.set('pack', chosen)
118 } else {
119 return { text: `No pack named "${arg}". Packs: ${PACK_IDS.join(', ')}, or random, off, reset.` }
120 }
121
122 $.ui.invalidate('ui.render')
123 const pack = currentPack()
124 if (custom !== undefined) return { text: 'Saved, but your `custom` words win while that option is set.' }
125 if (pack === undefined) return { text: 'Spinner pack off: Claude Code’s own words are back.' }
126 const label = chosen === undefined ? `back to your setting (${optionPack})` : `${pack.emoji} ${pack.title}`
127 return { text: `Spinner pack: ${label}. For example: ${samples(pack, 2).join(', ')}…` }
128 })
129}
130hooks/packs.ts 235 lines1// The word packs, as [while working, after the turn] pairs: the spinner says
2// "Plundering…", the line that closes the turn says "Plundered for 1m 3s".
3
4export type Pair = readonly [present: string, past: string | null]
5
6export type Pack = {
7 id: string
8 emoji: string
9 title: string
10 words: readonly Pair[]
11}
12
13export const PACKS: readonly Pack[] = [
14 {
15 id: 'pirate',
16 emoji: '🏴☠️',
17 title: 'Pirate',
18 words: [
19 ['Plundering', 'Plundered'], ['Hoisting the sails', 'Hoisted the sails'], ['Swabbing the deck', 'Swabbed the deck'],
20 ['Charting a course', 'Charted a course'], ['Weighing anchor', 'Weighed anchor'], ['Splicing the mainbrace', 'Spliced the mainbrace'],
21 ['Keelhauling bugs', 'Keelhauled bugs'], ['Burying treasure', 'Buried treasure'], ['Scanning the horizon', 'Scanned the horizon'],
22 ['Raising the Jolly Roger', 'Raised the Jolly Roger'], ['Battening the hatches', 'Battened the hatches'], ['Boarding', 'Boarded'],
23 ['Parleying', 'Parleyed'], ['Navigating', 'Navigated'], ['Tacking', 'Tacked'], ['Hauling', 'Hauled'],
24 ['Mending the rigging', 'Mended the rigging'], ['Counting doubloons', 'Counted doubloons'], ['Reading the map', 'Read the map'],
25 ['Pillaging', 'Pillaged'], ['Sailing', 'Sailed'], ['Dropping anchor', 'Dropped anchor'],
26 ["Climbing the crow's nest", "Climbed the crow's nest"], ['Polishing the cannons', 'Polished the cannons'],
27 ['Feeding the parrot', 'Fed the parrot'], ['Swashbuckling', 'Swashbuckled'], ['Yo-ho-ho-ing', 'Yo-ho-ho-ed'],
28 ],
29 },
30 {
31 id: 'shakespeare',
32 emoji: '🎭',
33 title: 'Shakespeare',
34 words: [
35 ['Pondering', 'Pondered'], ['Soliloquizing', 'Soliloquized'], ['Musing', 'Mused'], ['Contemplating', 'Contemplated'],
36 ['Versifying', 'Versified'], ['Declaiming', 'Declaimed'], ['Brooding', 'Brooded'], ['Rehearsing', 'Rehearsed'],
37 ['Composing a sonnet', 'Composed a sonnet'], ['Scheming', 'Schemed'], ['Lamenting', 'Lamented'], ['Beseeching', 'Beseeched'],
38 ['Proclaiming', 'Proclaimed'], ['Quilling', 'Quilled'], ['Wooing', 'Wooed'], ['Plotting', 'Plotted'],
39 ['Exeunting', 'Exeunted'], ['Perchancing', 'Perchanced'], ['Dreaming, perchance', 'Dreamt, perchance'],
40 ['Taking arms', 'Took arms'], ['Treading the boards', 'Trod the boards'], ['Scanning the meter', 'Scanned the meter'],
41 ['Consulting the witches', 'Consulted the witches'], ['Holding the mirror up', 'Held the mirror up'],
42 ['Mustering', 'Mustered'], ['Bestirring', 'Bestirred'], ['Forswearing', 'Forswore'],
43 ],
44 },
45 {
46 id: 'corporate',
47 emoji: '💼',
48 title: 'Corporate',
49 words: [
50 ['Synergizing', 'Synergized'], ['Circling back', 'Circled back'], ['Aligning stakeholders', 'Aligned stakeholders'],
51 ['Leveraging', 'Leveraged'], ['Ideating', 'Ideated'], ['Actioning', 'Actioned'], ['Deep-diving', 'Deep-dived'],
52 ['Moving the needle', 'Moved the needle'], ['Boiling the ocean', 'Boiled the ocean'], ['Taking it offline', 'Took it offline'],
53 ['Touching base', 'Touched base'], ['Pivoting', 'Pivoted'], ['Socializing the deck', 'Socialized the deck'],
54 ['Right-sizing', 'Right-sized'], ['Operationalizing', 'Operationalized'], ['Unpacking', 'Unpacked'],
55 ['Double-clicking', 'Double-clicked'], ['Parking it', 'Parked it'], ['Level-setting', 'Level-set'],
56 ['Optimizing synergies', 'Optimized synergies'], ['Drafting the roadmap', 'Drafted the roadmap'],
57 ['Scheduling a sync', 'Scheduled a sync'], ['Picking low-hanging fruit', 'Picked low-hanging fruit'],
58 ['Running it up the flagpole', 'Ran it up the flagpole'], ['Blue-sky thinking', 'Blue-sky thought'],
59 ['Shifting paradigms', 'Shifted paradigms'], ['Adding value', 'Added value'],
60 ],
61 },
62 {
63 id: 'wizard',
64 emoji: '🧙',
65 title: 'Wizard',
66 words: [
67 ['Conjuring', 'Conjured'], ['Enchanting', 'Enchanted'], ['Transmuting', 'Transmuted'], ['Brewing potions', 'Brewed potions'],
68 ['Scrying', 'Scried'], ['Summoning', 'Summoned'], ['Invoking', 'Invoked'], ['Spellcasting', 'Spellcast'],
69 ['Consulting the grimoire', 'Consulted the grimoire'], ['Charging the staff', 'Charged the staff'],
70 ['Reading the runes', 'Read the runes'], ['Warding', 'Warded'], ['Divining', 'Divined'], ['Levitating', 'Levitated'],
71 ['Polishing the orb', 'Polished the orb'], ['Stirring the cauldron', 'Stirred the cauldron'], ['Hexing bugs', 'Hexed bugs'],
72 ['Dispelling', 'Dispelled'], ['Channeling mana', 'Channeled mana'], ['Inscribing glyphs', 'Inscribed glyphs'],
73 ['Bewitching', 'Bewitched'], ['Alchemizing', 'Alchemized'], ['Gazing at the stars', 'Gazed at the stars'],
74 ['Whispering incantations', 'Whispered incantations'], ['Feeding the familiar', 'Fed the familiar'],
75 ['Tracing sigils', 'Traced sigils'], ['Opening a portal', 'Opened a portal'],
76 ],
77 },
78 {
79 id: 'chef',
80 emoji: '👩🍳',
81 title: 'Chef',
82 words: [
83 ['Simmering', 'Simmered'], ['Whisking', 'Whisked'], ['Braising', 'Braised'], ['Julienning', 'Julienned'],
84 ['Marinating', 'Marinated'], ['Kneading', 'Kneaded'], ['Flambéing', 'Flambéed'], ['Deglazing', 'Deglazed'],
85 ['Reducing', 'Reduced'], ['Plating', 'Plated'], ['Seasoning', 'Seasoned'], ['Proofing the dough', 'Proofed the dough'],
86 ['Folding', 'Folded'], ['Basting', 'Basted'], ['Caramelizing', 'Caramelized'], ['Blanching', 'Blanched'],
87 ['Emulsifying', 'Emulsified'], ['Tasting', 'Tasted'], ['Prepping the mise en place', 'Prepped the mise en place'],
88 ['Zesting', 'Zested'], ['Searing', 'Seared'], ['Glazing', 'Glazed'], ['Garnishing', 'Garnished'],
89 ['Tempering chocolate', 'Tempered chocolate'], ['Slow-roasting', 'Slow-roasted'], ['Fermenting', 'Fermented'],
90 ['Sharpening knives', 'Sharpened knives'],
91 ],
92 },
93 {
94 id: 'space',
95 emoji: '🚀',
96 title: 'Space',
97 words: [
98 ['Launching', 'Launched'], ['Orbiting', 'Orbited'], ['Calibrating thrusters', 'Calibrated thrusters'], ['Docking', 'Docked'],
99 ['Warping', 'Warped'], ['Charting stars', 'Charted stars'], ['Counting down', 'Counted down'],
100 ['Achieving liftoff', 'Achieved liftoff'], ['Slingshotting', 'Slingshotted'], ['Spacewalking', 'Spacewalked'],
101 ['Deploying solar panels', 'Deployed solar panels'], ['Pinging mission control', 'Pinged mission control'],
102 ['Re-entering', 'Re-entered'], ['Terraforming', 'Terraformed'], ['Scanning nebulae', 'Scanned nebulae'],
103 ['Aligning the dish', 'Aligned the dish'], ['Running telemetry', 'Ran telemetry'], ['Trimming the trajectory', 'Trimmed the trajectory'],
104 ['Firing boosters', 'Fired boosters'], ['Mapping craters', 'Mapped craters'], ['Collecting stardust', 'Collected stardust'],
105 ['Engaging hyperdrive', 'Engaged hyperdrive'], ['Escaping gravity', 'Escaped gravity'], ['Decoding signals', 'Decoded signals'],
106 ['Stargazing', 'Stargazed'], ['Rendezvousing', 'Rendezvoused'], ['Splashing down', 'Splashed down'],
107 ],
108 },
109 {
110 id: 'noir',
111 emoji: '🕵️',
112 title: 'Noir',
113 words: [
114 ['Sleuthing', 'Sleuthed'], ['Tailing a suspect', 'Tailed a suspect'], ['Dusting for prints', 'Dusted for prints'],
115 ['Following leads', 'Followed leads'], ['Working the case', 'Worked the case'], ['Shaking down sources', 'Shook down sources'],
116 ['Connecting the dots', 'Connected the dots'], ['Staking out', 'Staked out'], ['Interrogating', 'Interrogated'],
117 ['Reading the dossier', 'Read the dossier'], ['Lighting a match', 'Lit a match'], ['Pounding the pavement', 'Pounded the pavement'],
118 ['Brooding in the rain', 'Brooded in the rain'], ['Narrating', 'Narrated'], ['Checking alibis', 'Checked alibis'],
119 ['Cracking the case', 'Cracked the case'], ['Squinting through blinds', 'Squinted through blinds'],
120 ['Calling in a favor', 'Called in a favor'], ['Tipping the fedora', 'Tipped the fedora'], ['Sifting evidence', 'Sifted evidence'],
121 ['Pinning the board', 'Pinned the board'], ['Tracing the money', 'Traced the money'],
122 ['Rounding up the usual suspects', 'Rounded up the usual suspects'], ['Listening to the jazz', 'Listened to the jazz'],
123 ['Smelling a rat', 'Smelled a rat'], ['Closing in', 'Closed in'], ['Filing the report', 'Filed the report'],
124 ],
125 },
126 {
127 id: 'genz',
128 emoji: '✨',
129 title: 'Gen Z',
130 words: [
131 ['Cooking', 'Cooked'], ['Locking in', 'Locked in'], ['Vibing', 'Vibed'], ['Manifesting', 'Manifested'],
132 ['Slaying', 'Slayed'], ['Rizzing up the compiler', 'Rizzed up the compiler'], ['Glowing up', 'Glowed up'],
133 ['Understanding the assignment', 'Understood the assignment'], ['Main-charactering', 'Main-charactered'],
134 ['Speedrunning', 'Speedran'], ['Lowkey thinking', 'Lowkey thought'], ['Highkey shipping', 'Highkey shipped'],
135 ['Living rent-free', 'Lived rent-free'], ['Touching grass', 'Touched grass'], ['Sending it', 'Sent it'],
136 ['Eating', 'Ate'], ['Serving', 'Served'], ["Making it bussin'", "Made it bussin'"], ['Entering my era', 'Entered my era'],
137 ['Staying delulu', 'Stayed delulu'], ['Being so real', 'Was so real'], ['Giving', 'Gave'], ['Mogging bugs', 'Mogged bugs'],
138 ['Hard-launching', 'Hard-launched'], ['Soft-launching', 'Soft-launched'], ['Doomscrolling', 'Doomscrolled'], ['Yapping', 'Yapped'],
139 ],
140 },
141 {
142 id: 'zen',
143 emoji: '🍃',
144 title: 'Zen',
145 words: [
146 ['Breathing', 'Breathed'], ['Centering', 'Centered'], ['Raking the gravel', 'Raked the gravel'], ['Sipping tea', 'Sipped tea'],
147 ['Watching clouds', 'Watched clouds'], ['Letting go', 'Let go'], ['Being present', 'Was present'], ['Sitting with it', 'Sat with it'],
148 ['Balancing stones', 'Balanced stones'], ['Listening to rain', 'Listened to rain'], ['Folding paper cranes', 'Folded paper cranes'],
149 ['Pruning the bonsai', 'Pruned the bonsai'], ['Ringing the bell', 'Rang the bell'], ['Walking slowly', 'Walked slowly'],
150 ['Noticing', 'Noticed'], ['Emptying the cup', 'Emptied the cup'], ['Following the breath', 'Followed the breath'],
151 ['Settling', 'Settled'], ['Unfolding', 'Unfolded'], ['Meditating', 'Meditated'], ['Drifting', 'Drifted'], ['Grounding', 'Grounded'],
152 ['Returning to stillness', 'Returned to stillness'], ['Watering the garden', 'Watered the garden'], ['Lighting incense', 'Lit incense'],
153 ['Flowing', 'Flowed'], ['Sweeping the path', 'Swept the path'],
154 ],
155 },
156 {
157 id: 'retro',
158 emoji: '💾',
159 title: 'Retro',
160 words: [
161 ['Dialing up', 'Dialed up'], ['Defragmenting', 'Defragmented'], ['Rewinding the tape', 'Rewound the tape'],
162 ['Blowing on the cartridge', 'Blew on the cartridge'], ['Hacking the mainframe', 'Hacked the mainframe'], ['Compiling', 'Compiled'],
163 ['Buffering', 'Buffered'], ['Loading from cassette', 'Loaded from cassette'], ['Burning a CD', 'Burned a CD'],
164 ['Formatting the floppy', 'Formatted the floppy'], ['Reticulating splines', 'Reticulated splines'], ['Booting', 'Booted'],
165 ['Telnetting', 'Telnetted'], ['Pinging', 'Pinged'], ['Downloading more RAM', 'Downloaded more RAM'],
166 ['Adjusting the antenna', 'Adjusted the antenna'], ['Overclocking', 'Overclocked'], ['Saving to disk', 'Saved to disk'],
167 ['Entering cheat codes', 'Entered cheat codes'], ['Typing in BASIC', 'Typed in BASIC'], ['Waiting for the modem', 'Waited for the modem'],
168 ['Sorting the mixtape', 'Sorted the mixtape'], ['Clearing the cache', 'Cleared the cache'], ['Untangling the cables', 'Untangled the cables'],
169 ['Scanning for viruses', 'Scanned for viruses'], ['Writing to ROM', 'Wrote to ROM'], ['Inserting disk 2', 'Inserted disk 2'],
170 ],
171 },
172]
173
174export const PACK_IDS: readonly string[] = PACKS.map(p => p.id)
175
176export function packById(id: string): Pack | undefined {
177 return PACKS.find(p => p.id === id)
178}
179
180/**
181 * Parses the `custom` option: comma-separated words, each optionally
182 * `Present|Past` (`Frobbing|Frobbed`) to theme the closing line too.
183 */
184export function parseCustom(text: string): Pack | undefined {
185 const words: Pair[] = text
186 .split(',')
187 .map(w => w.trim())
188 .filter(w => w !== '')
189 .map(w => {
190 const [present, past] = w.split('|').map(s => s.trim())
191 return [present ?? w, past !== undefined && past !== '' ? past : null] as const
192 })
193 .filter(([present]) => present !== '')
194 if (words.length === 0) return undefined
195 return { id: 'custom', emoji: '🎨', title: 'Custom', words }
196}
197
198/** A small, stable string hash (FNV-1a), so a seed always picks the same word. */
199export function hash(text: string): number {
200 let h = 0x811c9dc5
201 for (let i = 0; i < text.length; i++) {
202 h ^= text.charCodeAt(i)
203 h = Math.imul(h, 0x01000193)
204 }
205 return h >>> 0
206}
207
208/** Which pair a seed picks (one per turn and spinner). */
209export function indexFor(pack: Pack, seed: string): number {
210 return hash(seed) % pack.words.length
211}
212
213/** The word the spinner shows for this seed. */
214export function presentFor(pack: Pack, seed: string): string {
215 return pack.words[indexFor(pack, seed)]![0]
216}
217
218/** The past form of the pair at `index`, so the closing line echoes the spinner. */
219export function pastAt(pack: Pack, index: number): string | undefined {
220 return pack.words[index % pack.words.length]?.[1] ?? undefined
221}
222
223/** The past form for the closing line, or undefined when the pack has none for it. */
224export function pastFor(pack: Pack, seed: string): string | undefined {
225 const withPast = pack.words.filter(([, past]) => past !== null)
226 if (withPast.length === 0) return undefined
227 return withPast[hash(seed) % withPast.length]![1] ?? undefined
228}
229
230/** Three sample words for the /spinner listing. */
231export function samples(pack: Pack, count = 3): string[] {
232 const step = Math.max(1, Math.floor(pack.words.length / count))
233 return Array.from({ length: Math.min(count, pack.words.length) }, (_, i) => pack.words[i * step]![0])
234}
235