Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 46 additions & 12 deletions app/components/landing-hero.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,48 @@
'use client'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add the required TSDoc file header.

TS/TSX files should include a TSDoc header with @fileoverview, @author, @copyright,
and @license tags. Place it above the 'use client' directive.

🤖 Prompt for AI Agents
In `@app/components/landing-hero.tsx` at line 1, Add a TSDoc file header block
above the existing 'use client' directive in this file: include `@fileoverview`
with a short description, `@author` with the author name, `@copyright` with
year/owner, and `@license` with the project's license identifier; ensure the
header is a proper multi-line TSDoc comment placed immediately before the 'use
client' line so tooling picks it up.


import { motion } from 'framer-motion'
import { useGSAP } from '@gsap/react'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { useRef } from 'react'
import { NetworkBackground } from './network-background'

gsap.registerPlugin(ScrollTrigger)

export function LandingHero() {
const heroRef = useRef<HTMLDivElement>(null)

useGSAP(() => {
const tl = gsap.timeline()

// Stagger entrance animation for child elements
tl.fromTo(
'.hero-element',
{ opacity: 0, y: 20 },
{
opacity: 1,
y: 0,
duration: 0.6,
ease: 'power2.out',
stagger: 0.1,
}
)

// Add ScrollTrigger for parallax effect
ScrollTrigger.create({
trigger: heroRef.current,
start: 'top bottom',
end: 'bottom top',
scrub: 1,
onUpdate: (self) => {
const progress = self.progress
gsap.set(heroRef.current, {
y: -progress * 50, // Parallax effect
scale: 1 + progress * 0.05, // Slight zoom
})
},
})
})
Comment on lines +14 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The useGSAP hook should be scoped to the heroRef to ensure proper cleanup of animations and especially the ScrollTrigger instance. ScrollTrigger.create() can cause memory leaks if not properly cleaned up when the component unmounts. By providing a scope, @gsap/react can automatically handle this for you.

    useGSAP(() => {
        const tl = gsap.timeline()

        // Stagger entrance animation for child elements
        tl.fromTo(
            '.hero-element',
            { opacity: 0, y: 20 },
            {
                opacity: 1,
                y: 0,
                duration: 0.6,
                ease: 'power2.out',
                stagger: 0.1,
            }
        )

        // Add ScrollTrigger for parallax effect
        ScrollTrigger.create({
            trigger: heroRef.current,
            start: 'top bottom',
            end: 'bottom top',
            scrub: 1,
            onUpdate: (self) => {
                const progress = self.progress
                gsap.set(heroRef.current, {
                    y: -progress * 50, // Parallax effect
                    scale: 1 + progress * 0.05, // Slight zoom
                })
            },
        })
    }, { scope: heroRef })

Comment on lines +12 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "landing-hero.tsx" -type f

Repository: ssdeanx/AgentStack

Length of output: 95


🏁 Script executed:

cat -n app/components/landing-hero.tsx | head -60

Repository: ssdeanx/AgentStack

Length of output: 2758


🌐 Web query:

useGSAP hook scope dependencies cleanup ScrollTrigger best practices

💡 Result:

Here are concise best-practices for a React useGSAP (or gsap.context) hook: scope, deps, and cleanup — with sources.

  1. Prefer gsap.context / useGSAP (not raw global gsap calls)
  • gsap.context collects animations & ScrollTriggers and optionally scopes selectors to a ref so cleanup is trivial. Use the context wrapper for all animations created during render. [1]
  1. Where to run — useLayoutEffect (or the provided useGSAP)
  • Create the context inside useLayoutEffect (or useGSAP which wraps the effect) so layout measurements are stable and React StrictMode double-invocations are handled. [1][5]
  1. Scoping selectors
  • Pass a DOM element or ref to gsap.context (or use the hook’s scope option) so selector strings apply only to descendants and you avoid many refs. [1][6]
  1. Dependencies and updates
  • Treat useGSAP’s second arg (or your internal useLayoutEffect deps) like useEffect deps: list values that should re-run the animation (or set revertOnUpdate if you want the previous context reverted on dependency change). If you need to avoid stale closures, pass reactive values as arguments to event/callbacks or use React patterns (useCallback/useEffectEvent) rather than omitting deps. [6][3]
  1. Cleanup / killing ScrollTriggers
  • Always revert the context on unmount (ctx.revert()). That reverts/kills animations and ScrollTriggers created inside the context. For ScrollTrigger instances you explicitly created and need custom behavior, you can call their kill(revert, allowAnimation) or use ScrollTrigger.clearScrollMemory as needed. [1][2][7]
  1. Animations created after initial context run
  • Animations created later (e.g., in event handlers, setTimeout, async callbacks) are not automatically in the context unless you add them via ctx.add(...) or wrap their creation inside ctx.add / ctx.method so they’ll be reverted/cleaned. Use ctx.add or ctx.add("name", fn) for event-driven animations. [1]
  1. Avoid memory leaks & stale DOM
  • Remove any non-GSAP listeners in the cleanup returned by the context (you can return a cleanup function from the context). If you re-create contexts on dep changes, consider revertOnUpdate or call ctx.revert() before creating a new context to avoid duplicated ScrollTriggers. [1]
  1. ScrollTrigger specifics
  • If you disable or want temporary behavior use ScrollTrigger.disable(); to fully remove instance use .kill(). Be mindful of pin-related DOM changes (use kill(revert) appropriately). If routing/refresh issues arise, consider ScrollTrigger.clearScrollMemory(). Also call ScrollTrigger.refresh() after DOM/layout changes if necessary. [2][4][7]
  1. Practical notes
  • Keep animation logic inside the context function or add it to the context; avoid creating GSAP timelines on every render. Use refs/state only as deps. Test in StrictMode. [1][5]

Sources

  • GSAP docs: gsap.context (useGSAP hook info), including scoping, ctx.add, ctx.revert. [1]
  • ScrollTrigger docs: .kill(), plugin guidance and APIs. [2][4]
  • Practical React/useGSAP guides & examples (community writeups explaining revertOnUpdate/useGSAP behavior). [6][5]

If you want, I can: a) show a minimal React + useGSAP example with proper deps and cleanup, or b) review your component code and point out issues. Which would you prefer?


Scope GSAP selectors, ensure single run, and clean up ScrollTrigger on unmount.

useGSAP() without dependencies or scope options reruns on every render, creating duplicate timelines and ScrollTriggers. The .hero-element selector is global and will animate all matching elements on the page, not just those within this component. Add a null guard, scope the context to heroRef to restrict selectors to descendants, set empty dependencies to run once, and explicitly clean up animations and ScrollTriggers on unmount.

Proposed fix
-    useGSAP(() => {
-        const tl = gsap.timeline()
+    useGSAP(() => {
+        if (!heroRef.current) return
+        const tl = gsap.timeline()
 
         // Stagger entrance animation for child elements
         tl.fromTo(
             '.hero-element',
             { opacity: 0, y: 20 },
             {
                 opacity: 1,
                 y: 0,
                 duration: 0.6,
                 ease: 'power2.out',
                 stagger: 0.1,
             }
         )
 
         // Add ScrollTrigger for parallax effect
-        ScrollTrigger.create({
+        const trigger = ScrollTrigger.create({
             trigger: heroRef.current,
             start: 'top bottom',
             end: 'bottom top',
             scrub: 1,
             onUpdate: (self) => {
                 const progress = self.progress
                 gsap.set(heroRef.current, {
                     y: -progress * 50, // Parallax effect
                     scale: 1 + progress * 0.05, // Slight zoom
                 })
             },
         })
-    })
+        return () => {
+            tl.kill()
+            trigger.kill()
+        }
+    }, { scope: heroRef, dependencies: [] })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const heroRef = useRef<HTMLDivElement>(null)
useGSAP(() => {
const tl = gsap.timeline()
// Stagger entrance animation for child elements
tl.fromTo(
'.hero-element',
{ opacity: 0, y: 20 },
{
opacity: 1,
y: 0,
duration: 0.6,
ease: 'power2.out',
stagger: 0.1,
}
)
// Add ScrollTrigger for parallax effect
ScrollTrigger.create({
trigger: heroRef.current,
start: 'top bottom',
end: 'bottom top',
scrub: 1,
onUpdate: (self) => {
const progress = self.progress
gsap.set(heroRef.current, {
y: -progress * 50, // Parallax effect
scale: 1 + progress * 0.05, // Slight zoom
})
},
})
})
const heroRef = useRef<HTMLDivElement>(null)
useGSAP(() => {
if (!heroRef.current) return
const tl = gsap.timeline()
// Stagger entrance animation for child elements
tl.fromTo(
'.hero-element',
{ opacity: 0, y: 20 },
{
opacity: 1,
y: 0,
duration: 0.6,
ease: 'power2.out',
stagger: 0.1,
}
)
// Add ScrollTrigger for parallax effect
const trigger = ScrollTrigger.create({
trigger: heroRef.current,
start: 'top bottom',
end: 'bottom top',
scrub: 1,
onUpdate: (self) => {
const progress = self.progress
gsap.set(heroRef.current, {
y: -progress * 50, // Parallax effect
scale: 1 + progress * 0.05, // Slight zoom
})
},
})
return () => {
tl.kill()
trigger.kill()
}
}, { scope: heroRef, dependencies: [] })
🤖 Prompt for AI Agents
In `@app/components/landing-hero.tsx` around lines 12 - 44, Scope the GSAP
animation to this component, guard against a null ref, run the effect only once,
and clean up ScrollTrigger on unmount: wrap the timeline and selector usage in a
gsap.context tied to heroRef (use context.selector or context() so
'.hero-element' targets only descendants), add a null check for heroRef.current
before creating ScrollTrigger, pass an empty dependency array to useGSAP to
ensure a single run, and return a cleanup that kills the timeline/context and
calls ScrollTrigger.getAll()/kill() or kills the specific ScrollTrigger instance
created (reference the ScrollTrigger returned from ScrollTrigger.create) to
remove listeners when the component unmounts.


return (
<section className="relative h-[800px] w-full overflow-hidden border-b border-border bg-background">
{/* Interactive System Visualization */}
Expand All @@ -14,40 +53,35 @@ export function LandingHero() {

<div className="container relative z-10 mx-auto flex h-full flex-col items-center justify-center px-4 text-center">
{/* Main Content */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="mx-auto max-w-4xl"
>
<div className="mb-6 inline-flex items-center rounded-full border border-white/10 bg-black/50 px-3 py-1 text-sm backdrop-blur-md shadow-lg shadow-black/20">
<div ref={heroRef} className="mx-auto max-w-4xl">
<div className="mb-6 inline-flex items-center rounded-full border border-white/10 bg-black/50 px-3 py-1 text-sm backdrop-blur-md shadow-lg shadow-black/20 hero-element">
<span className="flex size-2 me-2 rounded-full bg-green-500 animate-pulse"></span>
<span className="text-white/80 font-mono text-xs tracking-wider">
SYSTEM OPERATIONAL
</span>
</div>

<h1 className="mb-8 text-5xl font-bold tracking-tight text-foreground sm:text-6xl lg:text-8xl drop-shadow-2xl">
<h1 className="mb-8 text-5xl font-bold tracking-tight text-foreground sm:text-6xl lg:text-8xl drop-shadow-2xl hero-element">
Agent Orchestration
<br />
<span className="text-transparent bg-clip-text bg-linear-to-b from-white to-white/50">
Interactive Swarm
</span>
</h1>

<p className="mx-auto mb-12 max-w-xl text-lg leading-relaxed text-muted-foreground sm:text-xl">
<p className="mx-auto mb-12 max-w-xl text-lg leading-relaxed text-muted-foreground sm:text-xl hero-element">
Visualizing real-time agent interactions. Hover over the
nodes to interact with the neural network.
</p>

<div className="flex items-center justify-center gap-4">
<div className="flex items-center justify-center gap-4 hero-element">
<div className="h-px w-24 bg-linear-to-r from-transparent via-border to-transparent" />
<span className="text-xs text-muted-foreground font-mono uppercase tracking-widest">
Live Simulation
</span>
<div className="h-px w-24 bg-linear-to-r from-transparent via-border to-transparent" />
</div>
</motion.div>
</div>
</div>
</section>
)
Expand Down
Loading
Loading