diff --git a/cspell.config.yaml b/cspell.config.yaml
index af525c997..1bf52c8a2 100644
--- a/cspell.config.yaml
+++ b/cspell.config.yaml
@@ -166,6 +166,7 @@ words:
- qwen
- rehype
- reka
+ - Replayable
- resampler
- rlib
- rmcp
@@ -213,6 +214,7 @@ words:
- Volcengine
- vrma
- vueuse
+ - waapi
- wavefile
- webgpu
- weeb
diff --git a/packages/stage-ui/src/components/Animations/Replayable.vue b/packages/stage-ui/src/components/Animations/Replayable.vue
new file mode 100644
index 000000000..da6d44982
--- /dev/null
+++ b/packages/stage-ui/src/components/Animations/Replayable.vue
@@ -0,0 +1,190 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/stage-ui/src/components/Animations/use-replayable.ts b/packages/stage-ui/src/components/Animations/use-replayable.ts
new file mode 100644
index 000000000..5937aa9ea
--- /dev/null
+++ b/packages/stage-ui/src/components/Animations/use-replayable.ts
@@ -0,0 +1,38 @@
+import { inject, onUnmounted } from 'vue'
+
+interface ReplayableContext {
+ registerReplayCallback: (callback: () => void | Promise) => () => void
+ isReplaying: () => boolean
+}
+
+export function useReplayable(replayFn?: () => void | Promise) {
+ const context = inject('replayable')
+
+ if (!context) {
+ console.warn('useReplayable must be used within a Replayable component')
+
+ return {
+ registerReplay: () => () => {},
+ isReplaying: () => false,
+ }
+ }
+
+ const registerReplay = (callback: () => void | Promise) => {
+ return context.registerReplayCallback(callback)
+ }
+
+ // Auto-register if callback provided
+ let unregister: (() => void) | undefined
+ if (replayFn) {
+ unregister = registerReplay(replayFn)
+ }
+
+ onUnmounted(() => {
+ unregister?.()
+ })
+
+ return {
+ registerReplay,
+ isReplaying: context.isReplaying,
+ }
+}