diff --git a/packages/lint/src/rules/composition.test.ts b/packages/lint/src/rules/composition.test.ts
index fcf7457d6..5ac2ad48d 100644
--- a/packages/lint/src/rules/composition.test.ts
+++ b/packages/lint/src/rules/composition.test.ts
@@ -1159,6 +1159,20 @@ describe("composition rules", () => {
expect(find(result.findings)).toBeUndefined();
});
+ it("does not error when a GSAP timeline is registered with a computed bracket key", async () => {
+ const html = `
+
+
+ `;
+ const result = await lintHyperframeHtml(html);
+ expect(find(result.findings)).toBeUndefined();
+ });
+
it("does not error for a finite CSS animation (runtime auto-infers duration)", async () => {
const html = `
diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts
index d60992b2d..51f90ff22 100644
--- a/packages/lint/src/rules/gsap.test.ts
+++ b/packages/lint/src/rules/gsap.test.ts
@@ -1296,6 +1296,26 @@ describe("GSAP rules", () => {
expect(finding).toBeUndefined();
});
+ it("does NOT warn when timeline is registered with a computed bracket key", async () => {
+ const html = `
+
+
+
+
+`;
+ const result = await lintHyperframeHtml(html);
+ const finding = result.findings.find((f) => f.code === "gsap_timeline_not_registered");
+ expect(finding).toBeUndefined();
+ });
+
it("does NOT warn for sub-compositions (template-based)", async () => {
const html = `
diff --git a/packages/lint/src/utils.ts b/packages/lint/src/utils.ts
index 60296d2f0..86f053b4a 100644
--- a/packages/lint/src/utils.ts
+++ b/packages/lint/src/utils.ts
@@ -28,8 +28,16 @@ export const TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN =
/window\.__timelines\s*=\s*\{\s*(?:["'][^"']+["']|[A-Za-z_$][\w$]*)\s*:/i;
export const TIMELINE_REGISTRY_ASSIGN_PATTERN =
/window\.__timelines(?:\[[^\]]+\]|\.[A-Za-z_$][\w$]*)\s*=/i;
+// The bracket branch accepts either a quoted string key (`["root"]`) or a
+// computed key (`[spec.id]`, `[id]`) — a bare-identifier-only bracket branch
+// missed `window.__timelines[spec.id] = tl`, a pattern the shipped
+// code-particle-assemble/code-3d-extrude registry blocks actually use,
+// making gsap_timeline_not_registered false-fire on correctly registered
+// timelines. The computed-key alternative is deliberately non-capturing:
+// its text isn't a literal composition id, so callers reading group 1/2
+// (readRegisteredTimelineCompositionId) must keep falling back to null for it.
export const WINDOW_TIMELINE_ASSIGN_PATTERN =
- /window\.__timelines(?:\[\s*["']([^"']+)["']\s*\]|\.\s*([A-Za-z_$][\w$]*))\s*=\s*([A-Za-z_$][\w$]*)/i;
+ /window\.__timelines(?:\[\s*(?:["']([^"']+)["']|[A-Za-z_$][\w$.]*)\s*\]|\.\s*([A-Za-z_$][\w$]*))\s*=\s*([A-Za-z_$][\w$]*)/i;
export const INVALID_SCRIPT_CLOSE_PATTERN = /