From e3b34f988c684ff5c5043e3d66e4e6688f669ec2 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Tue, 30 May 2023 01:43:33 -0400
Subject: [PATCH 01/22] rewrite react-flow

---
 src/Animation.lua                  |  78 ----------
 src/Animations/Base.lua            |  14 ++
 src/Animations/Types/Spring.lua    |  99 ++++++++++++
 src/Animations/Types/Tween.lua     | 138 ++++++++++++++++
 src/Animations/init.lua            |   4 +
 src/Hooks/useAnimation.lua         |  90 +++++++++++
 src/Hooks/useGroupAnimation.lua    | 107 +++++++++++++
 src/Hooks/useLatest.lua            |  18 ---
 src/Hooks/useSequence.lua          |  55 -------
 src/Hooks/useSequenceAnimation.lua | 120 ++++++++++++++
 src/Hooks/useSpring.lua            |  75 ---------
 src/Hooks/useStorage.lua           |  19 ---
 src/Hooks/useTween.lua             |  89 -----------
 src/init.lua                       |  12 +-
 test/Test.lua                      | 242 ++++++++++++++++-------------
 15 files changed, 709 insertions(+), 451 deletions(-)
 delete mode 100644 src/Animation.lua
 create mode 100644 src/Animations/Base.lua
 create mode 100644 src/Animations/Types/Spring.lua
 create mode 100644 src/Animations/Types/Tween.lua
 create mode 100644 src/Animations/init.lua
 create mode 100644 src/Hooks/useAnimation.lua
 create mode 100644 src/Hooks/useGroupAnimation.lua
 delete mode 100644 src/Hooks/useLatest.lua
 delete mode 100644 src/Hooks/useSequence.lua
 create mode 100644 src/Hooks/useSequenceAnimation.lua
 delete mode 100644 src/Hooks/useSpring.lua
 delete mode 100644 src/Hooks/useStorage.lua
 delete mode 100644 src/Hooks/useTween.lua

diff --git a/src/Animation.lua b/src/Animation.lua
deleted file mode 100644
index fbd85cf..0000000
--- a/src/Animation.lua
+++ /dev/null
@@ -1,78 +0,0 @@
-local Hooks = script.Parent.Hooks
-
-local useTween = require(Hooks.useTween)
-local useSpring = require(Hooks.useSpring)
-
-export type SpringProps = {
-	start: number,
-	goal: number,
-	speed: number,
-	damper: number,
-}
-
-export type TweenProps = {
-	start: number,
-	goal: number,
-	tweenInfo: TweenInfo,
-}
-
-local function Animation(name: string, timeStep: number?, props: SpringProps | TweenProps)
-	local update
-	local value
-
-	local play, playReverse, stop
-
-	if name == "Tween" then
-		assert(props.start, "Tween requires a start value")
-		assert(props.goal, "Tween requires a goal value")
-		assert(props.tweenInfo, "Tween requires a tweenInfo")
-
-		value, update, stop = useTween(props.start, props.tweenInfo)
-		play = function()
-			update({
-				start = props.start,
-				goal = props.goal,
-			})
-		end
-
-		playReverse = function()
-			update({
-				start = props.goal,
-				goal = props.start,
-			})
-		end
-	elseif name == "Spring" then
-		assert(props.start, "Spring requires a start value")
-		assert(props.goal, "Spring requires a goal value")
-
-		value, update, stop = useSpring(props.start, props.speed, props.damper)
-		play = function()
-			update({
-				value = props.start,
-				goal = props.goal,
-				force = props.force,
-			})
-		end
-
-		playReverse = function()
-			update({
-				value = props.goal,
-				goal = props.start,
-				force = props.force,
-			})
-		end
-	else
-		assert(false, name .. " is not a valid animation type")
-	end
-
-	return {
-		timeStep = timeStep,
-		start = props.start,
-		value = value,
-		playReverse = playReverse,
-		play = play,
-		stop = stop,
-	}
-end
-
-return Animation
diff --git a/src/Animations/Base.lua b/src/Animations/Base.lua
new file mode 100644
index 0000000..43e06be
--- /dev/null
+++ b/src/Animations/Base.lua
@@ -0,0 +1,14 @@
+local BaseAnimation = {}
+
+function BaseAnimation.new()
+	local self = {}
+
+	self.listener = nil
+	self.SetListener = function(_, listener: (any) -> ())
+		self.listener = listener
+	end
+
+	return self
+end
+
+return BaseAnimation
diff --git a/src/Animations/Types/Spring.lua b/src/Animations/Types/Spring.lua
new file mode 100644
index 0000000..aa0d828
--- /dev/null
+++ b/src/Animations/Types/Spring.lua
@@ -0,0 +1,99 @@
+local RunService = game:GetService("RunService")
+
+local BaseAnimation = require(script.Parent.Parent.Base)
+local Promise = require(script.Parent.Parent.Parent.Promise)
+local SpringValue = require(script.Parent.Parent.Parent.Utility.SpringValue)
+
+local Spring = {}
+Spring.__index = Spring
+
+export type Spring = typeof(Spring.new())
+export type SpringProperties = {
+	damper: number?,
+	speed: number?,
+	from: any,
+	to: any,
+}
+
+local Springs = {} :: {
+	[Spring]: {
+		completed: () -> nil,
+		resolve: () -> (),
+	},
+}
+
+function Spring.new(props: SpringProperties)
+	local self = setmetatable(BaseAnimation.new(), Spring) :: Spring
+
+	self.props = props
+	self.player = nil
+
+	return self
+end
+
+function Spring:Play(from: any?)
+	if self.playing then
+		self:Stop()
+	end
+
+	local baseFromValue = from or self.props.start :: any
+	local baseToValue = self.props.target :: any
+
+	assert(baseFromValue, "No from value provided")
+	assert(baseToValue, "No to value provided")
+
+	if from == baseToValue then
+		return Promise.resolve()
+	end
+
+	local animation = Promise.new(function(resolve, _, onCancel)
+		local newSpring = SpringValue.new(baseFromValue, self.props.speed, self.props.damper)
+
+		newSpring:SetGoal(baseToValue)
+		Springs[newSpring] = {
+			resolve = resolve,
+			update = function()
+				if self.listener then
+					self.listener(newSpring:GetValue())
+				end
+			end,
+		}
+
+		onCancel(function()
+			Springs[newSpring] = nil
+			resolve()
+		end)
+	end)
+
+	self.playing = true
+	self.player = animation
+
+	return animation
+end
+
+function Spring:Stop()
+	if not self.playing then
+		return
+	end
+
+	if self.player then
+		self.player:cancel()
+		self.player = nil
+	end
+
+	self.playing = false
+end
+
+RunService:BindToRenderStep("UPDATE_REACT_SPRINGS", Enum.RenderPriority.First.Value, function(dt: number)
+	for spring, data in Springs do
+		local didUpdate = spring:Update(dt)
+		data.update()
+
+		if not didUpdate then
+			Springs[spring] = nil
+			data.resolve()
+		end
+	end
+end)
+
+return Spring
diff --git a/src/Animations/Types/Tween.lua b/src/Animations/Types/Tween.lua
new file mode 100644
index 0000000..01c22d0
--- /dev/null
+++ b/src/Animations/Types/Tween.lua
@@ -0,0 +1,138 @@
+local TweenService = game:GetService("TweenService")
+
+local BaseAnimation = require(script.Parent.Parent.Base)
+local Promise = require(script.Parent.Parent.Parent.Promise)
+local LinearValue = require(script.Parent.Parent.Parent.Utility.LinearValue)
+
+local Tween = {}
+Tween.__index = Tween
+
+export type Tween = typeof(Tween.new())
+export type TweenProperties = {
+	from: any,
+	to: any,
+}
+
+local function playTween(tweenInfo, callback: (number) -> nil, completed: () -> nil)
+	local numberValue = Instance.new("NumberValue")
+	numberValue.Value = 0
+	numberValue:GetPropertyChangedSignal("Value"):Connect(function()
+		callback(numberValue.Value)
+	end)
+
+	local tween = TweenService:Create(numberValue, tweenInfo, {
+		Value = 1,
+	})
+
+	tween.Completed:Connect(function()
+		numberValue:Destroy()
+		completed()
+	end)
+
+	return function()
+		tween:Play()
+	end, function()
+		numberValue:Destroy()
+		tween:Cancel()
+	end
+end
+
+function Tween.new(props: TweenProperties)
+	local self = setmetatable(BaseAnimation.new(), Tween) :: Tween
+
+	self.props = props
+	self.player = nil
+
+	return self
+end
+
+function Tween:Play(from: any?)
+	if self.playing then
+		self:Stop()
+	end
+
+	local tweenInfo = self.props.info :: TweenInfo
+	local baseFromValue = from or self.props.start :: any
+	local baseToValue = self.props.target :: any
+
+	-- start immediately will start the tween but not update the listener
+	local startImmediately = self.props.immediate == true
+	local delayTime = self.props.delay :: number
+
+	if not delayTime then
+		assert(startImmediately == false, "Cannot start immediately without a delay")
+	else
+		if startImmediately then
+			assert(delayTime < tweenInfo.Time, "DelayTime must be less than Time")
+		end
+	end
+
+	assert(baseFromValue, "No from value provided")
+	assert(baseToValue, "No to value provided")
+
+	assert(tweenInfo, "No tween info provided")
+	assert(tweenInfo.RepeatCount == 0, "RepeatCount must be 0")
+	assert(tweenInfo.Reverses == false, "Reverses must be false")
+	assert(tweenInfo.DelayTime == 0, "DelayTime must be 0")
+
+	if from == baseToValue then
+		return Promise.resolve()
+	end
+
+	local fromValue = LinearValue.fromValue(baseFromValue)
+	local toValue = LinearValue.fromValue(baseToValue)
+
+	local animation = Promise.new(function(resolve, _, onCancel)
+		local canUpdate = true
+
+		local play, cancel = playTween(tweenInfo, function(value)
+			if not canUpdate then
+				return
+			end
+
+			local newValue = fromValue:Lerp(toValue, value):ToValue()
+			self.listener(newValue)
+		end, function()
+			self.playing = false
+			self.player = nil
+			resolve()
+		end)
+
+		onCancel(cancel)
+
+		if not delayTime then
+			play()
+		else
+			if startImmediately then
+				canUpdate = false
+				play()
+
+				task.wait(delayTime)
+				canUpdate = true
+			else
+				task.wait(delayTime)
+				play()
+			end
+		end
+	end)
+
+	self.playing = true
+	self.player = animation
+
+	return animation
+end
+
+function Tween:Stop()
+	if not self.playing then
+		return
+	end
+
+	if self.player then
+		self.player:cancel()
+		self.player = nil
+	end
+
+	self.playing = false
+end
+
+return Tween
diff --git a/src/Animations/init.lua b/src/Animations/init.lua
new file mode 100644
index 0000000..2430dae
--- /dev/null
+++ b/src/Animations/init.lua
@@ -0,0 +1,4 @@
+return {
+	Spring = require(script.Types.Spring).new,
+	Tween = require(script.Types.Tween).new,
+}
diff --git a/src/Hooks/useAnimation.lua b/src/Hooks/useAnimation.lua
new file mode 100644
index 0000000..2b2bc7a
--- /dev/null
+++ b/src/Hooks/useAnimation.lua
@@ -0,0 +1,90 @@
+local Promise = require(script.Parent.Parent.Promise)
+
+local Roact = require(script.Parent.Parent.Roact)
+local useRef = Roact.useRef
+
+export type AnimationProps = {
+	[string]: any,
+}
+
+local Animation = {}
+Animation.__index = Animation
+
+function Animation.new(props: AnimationProps)
+	local self = setmetatable({}, Animation)
+
+	self.playing = false
+	self.listener = nil
+	self.animation = props
+
+	for name, animation in props do
+		animation:SetListener(function(value)
+			if self.listener then
+				self.listener(name, value)
+			end
+		end)
+	end
+
+	return self
+end
+
+function Animation:SetListener(listener: (string, any) -> ())
+	self.listener = listener
+end
+
+function Animation:Play(fromProps: AnimationProps)
+	if self.playing then
+		self:Stop()
+	end
+
+	local animation = Promise.new(function(resolve, _, onCancel)
+		local promises = {}
+
+		for name, animatable in self.animation do
+			local animationPromise = animatable:Play(fromProps[name])
+			table.insert(promises, animationPromise)
+		end
+
+		local awaiter = Promise.all(promises):andThen(resolve)
+
+		onCancel(function()
+			awaiter:cancel()
+
+			for _, promise in promises do
+				promise:cancel()
+			end
+		end)
+	end)
+
+	self.playing = true
+	self.player = animation
+
+	return animation
+end
+
+function Animation:Stop()
+	if not self.playing then
+		return
+	end
+
+	if self.player then
+		self.player:cancel()
+		self.player = nil
+	end
+
+	self.playing = false
+end
+
+local function useAnimation(props: AnimationProps)
+	local animation = useRef()
+	local current = animation.current
+
+	if not current then
+		current = Animation.new(props)
+		animation.current = current
+	end
+
+	return current
+end
+
+return useAnimation
diff --git a/src/Hooks/useGroupAnimation.lua b/src/Hooks/useGroupAnimation.lua
new file mode 100644
index 0000000..3d046e8
--- /dev/null
+++ b/src/Hooks/useGroupAnimation.lua
@@ -0,0 +1,107 @@
+local Roact = require(script.Parent.Parent.Roact)
+local useState = Roact.useState
+local useRef = Roact.useRef
+
+local GroupAnimationController = {}
+GroupAnimationController.__index = GroupAnimationController
+
+type StateSetters = {
+	[string]: (any) -> nil,
+}
+
+export type Animation = {
+	Play: (DefaultProperties) -> Animation,
+	Stop: () -> nil,
+}
+
+export type GroupAnimation = {
+	[string]: Animation,
+}
+
+export type DefaultProperties = {
+	[string]: any,
+}
+
+function GroupAnimationController.new(props: GroupAnimation, default: DefaultProperties, setters: StateSetters)
+	local self = setmetatable({}, GroupAnimationController)
+
+	self.currentState = "Default"
+	self.animations = props
+	self.state = default
+
+	for state, animation in props do
+		animation:SetListener(function(name, value)
+			if self.currentState ~= state then
+				-- warn("IGNORED UPDATE:", state, name, value)
+				return
+			end
+
+			-- warn("UPDATE:", state, name, value)
+			self.state[name] = value
+			setters[name](value)
+		end)
+	end
+
+	return self
+end
+
+function GroupAnimationController:Play(newState: string)
+	local animation = self.animations[newState]
+	assert(animation, `No animation found for state {newState}`)
+
+	self.currentState = newState
+
+	if self.currentAnimation then
+		self.currentAnimation:Stop()
+	end
+
+	self.currentAnimation = animation
+	animation:Play(self.state)
+end
+
+function GroupAnimationController:Stop()
+	if self.currentAnimation then
+		self.currentAnimation:Stop()
+		self.currentAnimation = nil
+	end
+end
+
+local function getStateContainer(defaults: DefaultProperties)
+	local setters = {}
+	local values = {}
+
+	for name, value in defaults do
+		local state, setState = useState(value)
+
+		setters[name] = setState
+		values[name] = state
+	end
+
+	return setters, values
+end
+
+local function useGroupAnimation(props: GroupAnimation, default: DefaultProperties)
+	local controller = useRef()
+	local current = controller.current
+	local setters, values = getStateContainer(default)
+
+	if not current then
+		local newController = GroupAnimationController.new(props, default, setters)
+
+		current = {
+			play = function(newState: string)
+				newController:Play(newState)
+			end,
+
+			stop = function()
+				newController:Stop()
+			end,
+		}
+
+		controller.current = current
+	end
+
+	return values, current.play, current.stop
+end
+
+return useGroupAnimation
diff --git a/src/Hooks/useLatest.lua b/src/Hooks/useLatest.lua
deleted file mode 100644
index 4f2dfb4..0000000
--- a/src/Hooks/useLatest.lua
+++ /dev/null
@@ -1,18 +0,0 @@
-local Roact = require(script.Parent.Parent.Roact)
-local useEffect = Roact.useEffect
-local useState = Roact.useState
-
-local function useLatest(...: any...)
-	local initialValue = select(1, ...)
-	local state, setState = useState(initialValue)
-
-	for _, value in { ... } do
-		useEffect(function()
-			setState(value)
-		end, { value })
-	end
-
-	return state
-end
-
-return useLatest
diff --git a/src/Hooks/useSequence.lua b/src/Hooks/useSequence.lua
deleted file mode 100644
index 7f27ed0..0000000
--- a/src/Hooks/useSequence.lua
+++ /dev/null
@@ -1,55 +0,0 @@
-local Roact = require(script.Parent.Parent.Roact)
-local useRef = Roact.useRef
-
-local Animation = require(script.Parent.Parent.Animation)
-export type Animation = typeof(Animation)
-
-local function makeSequence(animations: { [string]: Animation })
-	local currentSequence
-
-	return {
-		play = function(data: { reverse: boolean }?)
-			local reverse = data and data.reverse
-			local nonce = tick()
-			currentSequence = nonce
-
-			for _, animation in animations do
-				animation.useStart.current = true
-
-				task.delay(animation.timeStep or 0, function()
-					if currentSequence == nonce then
-						animation.useStart.current = false
-						animation[reverse and "playReverse" or "play"]()
-					end
-				end)
-			end
-		end,
-
-		stop = function()
-			currentSequence = nil
-			for _, animation in animations do
-				animation.stop()
-			end
-		end,
-	}
-end
-
-local function useSequence(animations: { [string]: Animation })
-	local sequence = {}
-	local sequenceRef = useRef()
-	local currentSequence = sequenceRef.current
-
-	if not currentSequence then
-		currentSequence = makeSequence(animations)
-		sequenceRef.current = currentSequence
-	end
-
-	for name, animation in animations do
-		animation.useStart = useRef(true)
-		sequence[name] = animation.useStart.current and animation.start or animation.value
-	end
-
-	return sequence, currentSequence.play, currentSequence.stop
-end
-
-return useSequence
diff --git a/src/Hooks/useSequenceAnimation.lua b/src/Hooks/useSequenceAnimation.lua
new file mode 100644
index 0000000..191f60b
--- /dev/null
+++ b/src/Hooks/useSequenceAnimation.lua
@@ -0,0 +1,120 @@
+local Promise = require(script.Parent.Parent.Promise)
+
+local Roact = require(script.Parent.Parent.Roact)
+local useRef = Roact.useRef
+
+export type SequenceProps = { { timestamp: number } | any }
+
+local Sequence = {}
+Sequence.__index = Sequence
+
+function Sequence.new(props: SequenceProps)
+	local self = setmetatable({}, Sequence)
+
+	self.playing = false
+	self.listener = nil
+	self.animation = props
+
+	table.sort(props, function(a, b)
+		return a.timestamp < b.timestamp
+	end)
+
+	local lastTimestamp
+
+	for _, animation in props do
+		local timestamp = animation.timestamp
+		if lastTimestamp == timestamp then
+			error("Duplicate timestamp found in sequence")
+		end
+
+		lastTimestamp = timestamp
+
+		for name, animatable in animation do
+			if name == "timestamp" then
+				continue
+			end
+
+			animatable:SetListener(function(value)
+				if self.listener then
+					self.listener(name, value)
+				end
+			end)
+		end
+	end
+
+	return self
+end
+
+function Sequence:SetListener(listener: (string, any) -> ())
+	self.listener = listener
+end
+
+function Sequence:Play(fromProps: SequenceProps)
+	if self.playing then
+		self:Stop()
+	end
+
+	local animation = Promise.new(function(resolve, _, onCancel)
+		local promises = {}
+
+		for _, sequenced in self.animation do
+			local animationPromise = Promise.delay(sequenced.timestamp):andThen(function()
+				local allAnimatables = {}
+
+				for name, animatable in sequenced do
+					if name == "timestamp" then
+						continue
+					end
+
+					table.insert(allAnimatables, animatable:Play(fromProps[name]))
+				end
+
+				return Promise.all(allAnimatables)
+			end)
+
+			table.insert(promises, animationPromise)
+		end
+
+		local awaiter = Promise.all(promises):andThen(resolve)
+
+		onCancel(function()
+			for _, promise in promises do
+				promise:cancel()
+			end
+
+			awaiter:cancel()
+		end)
+	end)
+
+	self.playing = true
+	self.player = animation
+
+	return animation
+end
+
+function Sequence:Stop()
+	if not self.playing then
+		return
+	end
+
+	if self.player then
+		self.player:cancel()
+		self.player = nil
+	end
+
+	self.playing = false
+end
+
+local function useSequenceAnimation(props: SequenceProps)
+	local animation = useRef()
+	local current = animation.current
+
+	if not current then
+		current = Sequence.new(props)
+		animation.current = current
+	end
+
+	return current
+end
+
+return useSequenceAnimation
diff --git a/src/Hooks/useSpring.lua b/src/Hooks/useSpring.lua
deleted file mode 100644
index e71df6c..0000000
--- a/src/Hooks/useSpring.lua
+++ /dev/null
@@ -1,75 +0,0 @@
-local RunService = game:GetService("RunService")
-
-local Roact = require(script.Parent.Parent.Roact)
-local useEffect = Roact.useEffect
-local useState = Roact.useState
-local useMemo = Roact.useMemo
-
-local Utility = script.Parent.Parent.Utility
-local LinearValue = require(Utility.LinearValue)
-local SpringValue = require(Utility.SpringValue)
-local SpringValues = {}
-
-RunService:UnbindFromRenderStep("UPDATE_REACT_SPRING")
-RunService:BindToRenderStep("UPDATE_REACT_SPRING", Enum.RenderPriority.First.Value, function(dt: number)
-	for spring, setValue in pairs(SpringValues) do
-		local didUpdate = spring:Update(dt)
-		local updatedValue = spring:GetValue()
-
-		if not didUpdate then
-			SpringValues[spring] = nil
-			updatedValue = spring:GetGoal()
-		end
-
-		setValue(updatedValue)
-	end
-end)
-
-local function makeSpring(
-	updateState: (any) -> (),
-	initial: LinearValue.LinearValueType,
-	speed: number?,
-	dampening: number?
-)
-	local springValue = SpringValue.new(initial, speed, dampening)
-
-	return {
-		play = function(animation: {
-			goal: LinearValue.LinearValueType?,
-			value: LinearValue.LinearValueType?,
-			force: LinearValue.LinearValueType?,
-		})
-			if animation.goal then
-				springValue:SetGoal(animation.goal)
-			end
-
-			if animation.value then
-				springValue:SetValue(animation.value)
-			end
-
-			if animation.force then
-				springValue:Impulse(animation.force)
-			end
-
-			springValue:Run(updateState)
-		end,
-
-		stop = function()
-			springValue:Stop()
-		end,
-	}
-end
-
-local function useSpring(initial: LinearValue.LinearValueType, speed: number?, dampening: number?)
-	speed = speed or 20
-	dampening = dampening or 1
-
-	local state, setState = useState(initial)
-	local spring = useMemo(function()
-		return makeSpring(setState, initial, speed, dampening)
-	end, { initial, speed, dampening })
-
-	return state, spring.play, spring.stop
-end
-
-return useSpring
diff --git a/src/Hooks/useStorage.lua b/src/Hooks/useStorage.lua
deleted file mode 100644
index e47f713..0000000
--- a/src/Hooks/useStorage.lua
+++ /dev/null
@@ -1,19 +0,0 @@
-local Roact = require(script.Parent.Parent.Roact)
-local useRef = Roact.useRef
-
-local function useStorage(name: string?)
-	name = name or "global"
-
-	local globalStack = useRef({})
-
-	local currenStack = globalStack.current
-	local stack = currenStack[name]
-	if not stack then
-		stack = {}
-		currenStack[name] = stack
-	end
-
-	return stack
-end
-
-return useStorage
diff --git a/src/Hooks/useTween.lua b/src/Hooks/useTween.lua
deleted file mode 100644
index 5728ea1..0000000
--- a/src/Hooks/useTween.lua
+++ /dev/null
@@ -1,89 +0,0 @@
-local TweenService = game:GetService("TweenService")
-
-local Roact = require(script.Parent.Parent.Roact)
-local useRef = Roact.useRef
-local useState = Roact.useState
-local useEffect = Roact.useEffect
-
-local LinearValue = require(script.Parent.Parent.Utility.LinearValue)
-type LinearValue = typeof(LinearValue)
-
-local function makeTween(updateState: (any) -> (), initial: LinearValue.LinearValueType, tweenInfo: TweenInfo)
-	local start, goal, tween
-	local current = LinearValue.fromValue(initial)
-
-	local value = Instance.new("NumberValue")
-	value:GetPropertyChangedSignal("Value"):Connect(function()
-		current = start:Lerp(goal, value.Value)
-		updateState(current:ToValue())
-	end)
-
-	return {
-		destroy = function()
-			value:Destroy()
-		end,
-
-		play = function(animation: {
-			start: LinearValue.LinearValueType?,
-			goal: LinearValue.LinearValueType?,
-		})
-			local startValue = animation.start
-			local endValue = animation.goal
-
-			local currentValue = current:ToValue()
-
-			if startValue == currentValue and endValue == currentValue then
-				return
-			end
-
-			if not startValue then
-				startValue = currentValue
-			end
-
-			if tween then
-				tween:Cancel()
-			end
-
-			start = LinearValue.fromValue(startValue)
-			goal = LinearValue.fromValue(endValue)
-
-			value.Value = 0
-
-			tween = TweenService:Create(value, tweenInfo, { Value = 1 })
-			tween:Play()
-		end,
-
-		stop = function()
-			if tween then
-				tween:Cancel()
-				tween = nil
-			end
-		end,
-	}
-end
-
-local function useTween(initial: LinearValue.LinearValueType, tweenInfo: TweenInfo)
-	local current, setState = useState(initial)
-
-	local tweenRef = useRef()
-	local currentTweenRef = tweenRef.current
-
-	if not currentTweenRef then
-		currentTweenRef = makeTween(setState, initial, tweenInfo)
-		tweenRef.current = currentTweenRef
-	end
-
-	useEffect(function()
-		return function()
-			local currentTween = tweenRef.current
-			if currentTween then
-				currentTween:destroy()
-				tweenRef.current = nil
-			end
-		end
-	end, {})
-
-	return current, currentTweenRef.play, currentTweenRef.stop
-end
-
-return useTween
diff --git a/src/init.lua b/src/init.lua
index bb303a8..a06edcc 100644
--- a/src/init.lua
+++ b/src/init.lua
@@ -1,8 +1,10 @@
+local Animations = require(script.Animations)
+
 return {
-	Animation = require(script.Animation),
+	Tween = Animations.Tween,
+	Spring = Animations.Spring,
 
-	useLatest = require(script.Hooks.useLatest),
-	useSequence = require(script.Hooks.useSequence),
-	useSpring = require(script.Hooks.useSpring),
-	useTween = require(script.Hooks.useTween),
+	useAnimation = require(script.Hooks.useAnimation),
+	useGroupAnimation = require(script.Hooks.useGroupAnimation),
+	useSequenceAnimation = require(script.Hooks.useSequenceAnimation),
 }
diff --git a/test/Test.lua b/test/Test.lua
index 28f0fa4..e55e0eb 100644
--- a/test/Test.lua
+++ b/test/Test.lua
@@ -6,20 +6,25 @@ local useEffect = Roact.useEffect
 local createElement = Roact.createElement
 
 local ReactAnimation = require(Packages.RoactAnimation)
-local Animation = ReactAnimation.Animation
-local useTween = ReactAnimation.useTween
-local useSpring = ReactAnimation.useSpring
-local useSequence = ReactAnimation.useSequence
-local useLatest = ReactAnimation.useLatest
+local useGroupAnimation = ReactAnimation.useGroupAnimation
+local useSequenceAnimation = ReactAnimation.useSequenceAnimation
+local useAnimation = ReactAnimation.useAnimation
+-- local Spring = ReactAnimation.Spring
+local Tween = ReactAnimation.Tween
 
 -- @helpers
+local RNG = Random.new()
+
 local function createFrame(props, children)
 	props.Size = props.Size or UDim2.fromScale(1, 1)
 	props.SizeConstraint = Enum.SizeConstraint.RelativeXX
 	props.Text = props.Name
 
+	local parentSize = props.ParentSize or UDim2.fromScale(0.1, 0.25)
+	props.ParentSize = nil
+
 	return createElement("Frame", {
-		Size = UDim2.fromScale(0.1, 0.25),
+		Size = parentSize,
 		AnchorPoint = Vector2.new(0.5, 0),
 		BackgroundTransparency = 1,
 	}, {
@@ -30,129 +35,143 @@ end
 -- @tests
 -- @TestSequence
 local function TestSequence()
-	local sequence, play, stop = useSequence({
-		position = Animation(
-			"Spring",
-			0,
-			{ start = UDim2.fromScale(-1, 0), goal = UDim2.fromScale(1, 0), speed = 20, damper = 0.7 }
-		),
-
-		size = Animation(
-			"Tween",
-			0.2,
-			{ start = UDim2.fromScale(0.5, 0.5), goal = UDim2.fromScale(1.2, 1.2), tweenInfo = TweenInfo.new(1) }
-		),
-	})
-
-	useEffect(function()
-		play()
-
-		return stop
-	end, {})
-
-	return createFrame({
-		Name = "Sequence",
-		Position = sequence.position,
-		Size = sequence.size,
-	})
-end
-
--- @TestPriority
-local function TestPriority()
-	local sequence1, play1 = useSequence({
-		position = Animation(
-			"Spring",
-			0,
-			{ start = UDim2.fromScale(-1, 0), goal = UDim2.fromScale(1, 0), speed = 20, damper = 0.7 }
-		),
-
-		size = Animation(
-			"Tween",
-			1,
-			{ start = UDim2.fromScale(0.5, 0.5), goal = UDim2.fromScale(1.2, 1.2), tweenInfo = TweenInfo.new(1) }
-		),
-	})
-
-	local sequence2, play2 = useSequence({
-		position = Animation(
-			"Spring",
-			0.1,
-			{ start = UDim2.fromScale(1, 0), goal = UDim2.fromScale(-1, 0), speed = 20, damper = 0.7 }
-		),
-
-		size = Animation(
-			"Tween",
-			0.2,
-			{ start = UDim2.fromScale(0.7, 0.7), goal = UDim2.fromScale(0.1, 0.1), tweenInfo = TweenInfo.new(1) }
-		),
-	})
-
-	useEffect(function()
-		play1()
-		play2()
-	end, {})
-
-	return createFrame({
-		Name = "Priority",
-		Position = useLatest(sequence1.position, sequence2.position),
-		Size = useLatest(sequence1.size, sequence2.size),
-	})
-end
-
--- @TestTween
-local function TestTween()
-	local value, update = useTween(UDim2.fromScale(0, 0), TweenInfo.new(1))
-	local value2, update2 = useTween(Color3.new(), TweenInfo.new(1))
-
-	if value.X.Scale == 0 then
-		update({ goal = UDim2.fromScale(1, 0) })
-	elseif (value :: UDim2).X.Scale == 1 then
-		update({ goal = UDim2.fromScale(0, 0) })
-	end
-
-	if value2.R == 0 then
-		update2({ start = Color3.new(1, 0, 0), goal = Color3.new(1, 0, 0) })
-	elseif value2.R == 1 then
-		update2({ goal = Color3.new(0, 0, 0) })
-	end
-
-	return createFrame({
-		Name = "Tween",
-		Position = value,
-		BackgroundColor3 = value2,
+	-- local sequence, play, stop = useGroupAnimation({
+	-- 	moveRight = useAnimation({
+	-- 		-- position = Spring({
+	-- 		-- 	target = UDim2.fromScale(0.8, 0),
+	-- 		-- 	speed = 5,
+	-- 		-- 	damper = 0.7,
+	-- 		-- }),
+
+	-- 		position = Tween({
+	-- 			target = UDim2.fromScale(0.8, 0),
+	-- 			info = TweenInfo.new(1, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut),
+	-- 		}),
+	-- 	}),
+
+	-- 	moveLeft = useAnimation({
+	-- 		-- position = Spring({
+	-- 		-- 	target = UDim2.fromScale(0.3, 0),
+	-- 		-- 	speed = 5,
+	-- 		-- 	damper = 0.7,
+	-- 		-- }),
+
+	-- 		position = Tween({
+	-- 			target = UDim2.fromScale(0.3, 0),
+	-- 			info = TweenInfo.new(1, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut),
+	-- 		}),
+	-- 	}),
+	-- }, {
+	-- 	position = UDim2.fromScale(0.5, 0),
+	-- })
+
+	local sequence, play, stop = useGroupAnimation({
+		yo = useSequenceAnimation({
+			{
+				timestamp = 0,
+				size = Tween({
+					target = UDim2.fromOffset(400, 400),
+					info = TweenInfo.new(1, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut),
+				}),
+			},
+			{
+				timestamp = 1,
+				position = Tween({
+					target = UDim2.fromScale(0.3, 0),
+					info = TweenInfo.new(1, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut),
+				}),
+			},
+			{
+				timestamp = 2,
+				position = Tween({
+					target = UDim2.fromScale(0.8, 0),
+					info = TweenInfo.new(1, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut),
+				}),
+			},
+		}),
+	}, {
+		position = UDim2.fromScale(0.5, 0),
+		size = UDim2.fromOffset(200, 200),
 	})
-end
-
--- @TestSpring
-local function TestSpring()
-	local value, update = useSpring(UDim2.fromScale(-1, 0), 10, 0.5)
 
 	useEffect(function()
 		local running = true
 
 		task.spawn(function()
 			while running do
-				update({ goal = UDim2.fromScale(1, 0) })
+				play("yo")
 				task.wait(3)
-
-				if running then
-					update({ goal = UDim2.fromScale(-1, 0) })
-					task.wait(3)
-				end
 			end
 		end)
 
 		return function()
 			running = false
+			stop()
 		end
 	end, {})
 
 	return createFrame({
-		Name = "Spring",
-		Position = value,
+		Name = "Sequence",
+		Position = sequence.position,
+		Size = sequence.size,
+		ParentSize = UDim2.fromScale(1, 0.25),
 	})
 end
 
+-- -- @TestTween
+-- local function TestTween()
+-- 	local value, update = useTween(UDim2.fromScale(0, 0), TweenInfo.new(1))
+-- 	local value2, update2 = useTween(Color3.new(), TweenInfo.new(1))
+
+-- 	if value.X.Scale == 0 then
+-- 		update({ goal = UDim2.fromScale(1, 0) })
+-- 	elseif (value :: UDim2).X.Scale == 1 then
+-- 		update({ goal = UDim2.fromScale(0, 0) })
+-- 	end
+
+-- 	if value2.R == 0 then
+-- 		update2({ start = Color3.new(1, 0, 0), goal = Color3.new(1, 0, 0) })
+-- 	elseif value2.R == 1 then
+-- 		update2({ goal = Color3.new(0, 0, 0) })
+-- 	end
+
+-- 	return createFrame({
+-- 		Name = "Tween",
+-- 		Position = value,
+-- 		BackgroundColor3 = value2,
+-- 	})
+-- end
+
+-- -- @TestSpring
+-- local function TestSpring()
+-- 	local value, update = useSpring(UDim2.fromScale(-1, 0), 10, 0.5)
+
+-- 	useEffect(function()
+-- 		local running = true
+
+-- 		task.spawn(function()
+-- 			while running do
+-- 				update({ goal = UDim2.fromScale(1, 0) })
+-- 				task.wait(3)
+
+-- 				if running then
+-- 					update({ goal = UDim2.fromScale(-1, 0) })
+-- 					task.wait(3)
+-- 				end
+-- 			end
+-- 		end)
+
+-- 		return function()
+-- 			running = false
+-- 		end
+-- 	end, {})
+
+-- 	return createFrame({
+-- 		Name = "Spring",
+-- 		Position = value,
+-- 	})
+-- end
+
 -- @entry
 local function Test()
 	return createElement("Frame", {
@@ -164,10 +183,9 @@ local function Test()
 			VerticalAlignment = Enum.VerticalAlignment.Center,
 		}),
 
-		tween = createElement(TestTween),
-		spring = createElement(TestSpring),
+		-- tween = createElement(TestTween),
+		-- spring = createElement(TestSpring),
 		sequence = createElement(TestSequence),
-		priority = createElement(TestPriority),
 	})
 end
 

From b5acc6cb663bf60e956ccd3609afe0ef08018465 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Tue, 30 May 2023 01:46:01 -0400
Subject: [PATCH 02/22] Update roact->react

---
 README.md                          |  2 +-
 default.project.json               |  2 +-
 src/Hooks/useAnimation.lua         |  4 ++--
 src/Hooks/useGroupAnimation.lua    |  6 +++---
 src/Hooks/useSequenceAnimation.lua |  4 ++--
 src/React.lua                      |  1 +
 src/Roact.lua                      |  1 -
 test.project.json                  |  4 ++--
 test/Test.lua                      |  8 ++++----
 test/init.client.lua               |  8 ++++----
 test/init.story.lua                | 12 ++++++------
 wally.toml                         |  4 ++--
 12 files changed, 28 insertions(+), 28 deletions(-)
 create mode 100644 src/React.lua
 delete mode 100644 src/Roact.lua

diff --git a/README.md b/README.md
index cc7e567..e3c7b14 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-# Roact-Flow
+# React-Flow
diff --git a/default.project.json b/default.project.json
index 9d964fb..3ab210f 100644
--- a/default.project.json
+++ b/default.project.json
@@ -1,5 +1,5 @@
 {
-  "name": "roact-flow",
+  "name": "react-flow",
   "tree": {
     "$path": "src"
   }
diff --git a/src/Hooks/useAnimation.lua b/src/Hooks/useAnimation.lua
index 2b2bc7a..cc2d46c 100644
--- a/src/Hooks/useAnimation.lua
+++ b/src/Hooks/useAnimation.lua
@@ -1,7 +1,7 @@
 local Promise = require(script.Parent.Parent.Promise)
 
-local Roact = require(script.Parent.Parent.Roact)
-local useRef = Roact.useRef
+local React = require(script.Parent.Parent.React)
+local useRef = React.useRef
 
 export type AnimationProps = {
 	[string]: any,
diff --git a/src/Hooks/useGroupAnimation.lua b/src/Hooks/useGroupAnimation.lua
index 3d046e8..2db5e64 100644
--- a/src/Hooks/useGroupAnimation.lua
+++ b/src/Hooks/useGroupAnimation.lua
@@ -1,6 +1,6 @@
-local Roact = require(script.Parent.Parent.Roact)
-local useState = Roact.useState
-local useRef = Roact.useRef
+local React = require(script.Parent.Parent.React)
+local useState = React.useState
+local useRef = React.useRef
 
 local GroupAnimationController = {}
 GroupAnimationController.__index = GroupAnimationController
diff --git a/src/Hooks/useSequenceAnimation.lua b/src/Hooks/useSequenceAnimation.lua
index 191f60b..532fd2f 100644
--- a/src/Hooks/useSequenceAnimation.lua
+++ b/src/Hooks/useSequenceAnimation.lua
@@ -1,7 +1,7 @@
 local Promise = require(script.Parent.Parent.Promise)
 
-local Roact = require(script.Parent.Parent.Roact)
-local useRef = Roact.useRef
+local React = require(script.Parent.Parent.React)
+local useRef = React.useRef
 
 export type SequenceProps = { { timestamp: number } | any }
 
diff --git a/src/React.lua b/src/React.lua
new file mode 100644
index 0000000..45ac990
--- /dev/null
+++ b/src/React.lua
@@ -0,0 +1 @@
+return require(script.Parent.Parent.React)
diff --git a/src/Roact.lua b/src/Roact.lua
deleted file mode 100644
index e189fb4..0000000
--- a/src/Roact.lua
+++ /dev/null
@@ -1 +0,0 @@
-return require(script.Parent.Parent.Roact)
diff --git a/test.project.json b/test.project.json
index da44991..7bcd5be 100644
--- a/test.project.json
+++ b/test.project.json
@@ -1,5 +1,5 @@
 {
-  "name": "Roact-Flow Animation Test",
+  "name": "React-Flow Animation Test",
   "tree": {
     "$className": "DataModel",
     "ReplicatedStorage": {
@@ -9,7 +9,7 @@
         "$path": "Packages"
       },
 
-      "RoactAnimation": {
+      "ReactAnimation": {
         "$path": "src"
       }
     },
diff --git a/test/Test.lua b/test/Test.lua
index e55e0eb..69a41e2 100644
--- a/test/Test.lua
+++ b/test/Test.lua
@@ -1,11 +1,11 @@
 local ReplicatedStorage = game:GetService("ReplicatedStorage")
 local Packages = ReplicatedStorage:WaitForChild("Packages")
 
-local Roact = require(Packages.Roact)
-local useEffect = Roact.useEffect
-local createElement = Roact.createElement
+local React = require(Packages.React)
+local useEffect = React.useEffect
+local createElement = React.createElement
 
-local ReactAnimation = require(Packages.RoactAnimation)
+local ReactAnimation = require(Packages.ReactAnimation)
 local useGroupAnimation = ReactAnimation.useGroupAnimation
 local useSequenceAnimation = ReactAnimation.useSequenceAnimation
 local useAnimation = ReactAnimation.useAnimation
diff --git a/test/init.client.lua b/test/init.client.lua
index 13ceb54..59a2726 100644
--- a/test/init.client.lua
+++ b/test/init.client.lua
@@ -3,13 +3,13 @@ local Players = game:GetService("Players")
 local LocalPlayer = Players.LocalPlayer
 
 local Packages = ReplicatedStorage:WaitForChild("Packages")
-ReplicatedStorage.RoactAnimation.Parent = Packages
+ReplicatedStorage.ReactAnimation.Parent = Packages
 
 local Test = require(script.Test)
 
-local Roact = require(Packages.Roact)
-local createElement = Roact.createElement
-local mount = Roact.mount
+local React = require(Packages.React)
+local createElement = React.createElement
+local mount = React.mount
 
 if not LocalPlayer.Character then
 	LocalPlayer.CharacterAdded:Wait()
diff --git a/test/init.story.lua b/test/init.story.lua
index ccb5cde..7c54d5a 100644
--- a/test/init.story.lua
+++ b/test/init.story.lua
@@ -1,19 +1,19 @@
 local ReplicatedStorage = game:GetService("ReplicatedStorage")
 local Packages = ReplicatedStorage.Packages
 
-local Roact = require(Packages.Roact)
-local createElement = Roact.createElement
-local mount, unmount = Roact.mount, Roact.unmount
+local React = require(Packages.React)
+local createElement = React.createElement
+local mount, unmount = React.mount, React.unmount
 
 return function(container)
-	local roactAnimation = ReplicatedStorage.RoactAnimation
-	roactAnimation.Parent = Packages
+	local ReactAnimation = ReplicatedStorage.ReactAnimation
+	ReactAnimation.Parent = Packages
 
 	local Test = require(script.Parent.Test)
 	local root = mount(createElement(Test), container)
 
 	return function()
 		unmount(root)
-		roactAnimation.Parent = ReplicatedStorage
+		ReactAnimation.Parent = ReplicatedStorage
 	end
 end
diff --git a/wally.toml b/wally.toml
index 768f184..02f8bd4 100644
--- a/wally.toml
+++ b/wally.toml
@@ -1,10 +1,10 @@
 [package]
-name = "nexure/roact-animation"
+name = "nexure/react-animation"
 version = "0.1.1"
 registry = "https://github.com/UpliftGames/wally-index"
 realm = "shared"
 
 [dependencies]
-Roact = "core-packages/roact-compat@17.0.1-rc.16.1"
+React = "core-packages/roact-compat@17.0.1-rc.16.1"
 Promise = "evaera/promise@4.0.0"
 

From 9132c6e202bfc3a694283606198ac4ca061ac3e2 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Thu, 6 Jul 2023 14:33:35 -0400
Subject: [PATCH 03/22] Update entrypoint

---
 test/init.client.lua | 5 +++--
 test/init.story.lua  | 7 ++++---
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/test/init.client.lua b/test/init.client.lua
index 59a2726..73352c2 100644
--- a/test/init.client.lua
+++ b/test/init.client.lua
@@ -8,12 +8,13 @@ ReplicatedStorage.ReactAnimation.Parent = Packages
 local Test = require(script.Test)
 
 local React = require(Packages.React)
+local ReactRoblox = require(Packages.ReactRoblox)
 local createElement = React.createElement
-local mount = React.mount
 
 if not LocalPlayer.Character then
 	LocalPlayer.CharacterAdded:Wait()
 	task.wait(4)
 end
 
-mount(createElement("ScreenGui", {}, { Test = createElement(Test) }), LocalPlayer:WaitForChild("PlayerGui"))
+local root = ReactRoblox.createRoot(LocalPlayer:WaitForChild("PlayerGui"))
+root:render(createElement("ScreenGui", {}, { Test = createElement(Test) }))
diff --git a/test/init.story.lua b/test/init.story.lua
index 7c54d5a..bc1b30f 100644
--- a/test/init.story.lua
+++ b/test/init.story.lua
@@ -2,18 +2,19 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
 local Packages = ReplicatedStorage.Packages
 
 local React = require(Packages.React)
+local ReactRoblox = require(Packages.ReactRoblox)
 local createElement = React.createElement
-local mount, unmount = React.mount, React.unmount
 
 return function(container)
 	local ReactAnimation = ReplicatedStorage.ReactAnimation
 	ReactAnimation.Parent = Packages
 
 	local Test = require(script.Parent.Test)
-	local root = mount(createElement(Test), container)
+	local root = ReactRoblox.createRoot(container)
+	root:render(createElement(Test))
 
 	return function()
-		unmount(root)
 		ReactAnimation.Parent = ReplicatedStorage
+		root:unmount()
 	end
 end

From 1ed3454c8d4d1423aa9e38156d84debd24b8cf22 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Thu, 6 Jul 2023 14:33:43 -0400
Subject: [PATCH 04/22] Update to use regular react

---
 wally.toml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/wally.toml b/wally.toml
index 02f8bd4..d549419 100644
--- a/wally.toml
+++ b/wally.toml
@@ -5,6 +5,8 @@ registry = "https://github.com/UpliftGames/wally-index"
 realm = "shared"
 
 [dependencies]
-React = "core-packages/roact-compat@17.0.1-rc.16.1"
+
+React = "core-packages/react@17.0.1-rc.18"
+ReactRoblox = "core-packages/react-roblox@17.0.1-rc.16"
 Promise = "evaera/promise@4.0.0"
 

From 1cb08882caeb01d6c9bd5585f43611a3af841c08 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Thu, 6 Jul 2023 14:34:50 -0400
Subject: [PATCH 05/22] Update to use bindings

---
 src/Hooks/useGroupAnimation.lua | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/Hooks/useGroupAnimation.lua b/src/Hooks/useGroupAnimation.lua
index 2db5e64..d8f996e 100644
--- a/src/Hooks/useGroupAnimation.lua
+++ b/src/Hooks/useGroupAnimation.lua
@@ -1,5 +1,5 @@
 local React = require(script.Parent.Parent.React)
-local useState = React.useState
+local useBinding = React.useBinding
 local useRef = React.useRef
 
 local GroupAnimationController = {}
@@ -71,10 +71,10 @@ local function getStateContainer(defaults: DefaultProperties)
 	local values = {}
 
 	for name, value in defaults do
-		local state, setState = useState(value)
+		local binding, updateBinding = useBinding(value)
 
-		setters[name] = setState
-		values[name] = state
+		setters[name] = updateBinding
+		values[name] = binding
 	end
 
 	return setters, values

From dc47a74779224e704a15e25a0eff002d5f99be40 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Thu, 6 Jul 2023 15:20:03 -0400
Subject: [PATCH 06/22] Add useSpring & useTween

---
 src/Hooks/useSpring.lua | 41 ++++++++++++++++++++++++++++++++++++++
 src/Hooks/useTween.lua  | 44 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 85 insertions(+)
 create mode 100644 src/Hooks/useSpring.lua
 create mode 100644 src/Hooks/useTween.lua

diff --git a/src/Hooks/useSpring.lua b/src/Hooks/useSpring.lua
new file mode 100644
index 0000000..a72ab06
--- /dev/null
+++ b/src/Hooks/useSpring.lua
@@ -0,0 +1,41 @@
+local React = require(script.Parent.Parent.React)
+local useRef = React.useRef
+local createBinding = React.createBinding
+
+local Spring = require(script.Parent.Parent.Animations.Types.Spring)
+
+local function makeSpring(props: Spring.SpringProperties, updater: (any) -> nil)
+	local newSpring = Spring.new(props)
+	newSpring:SetListener(updater)
+
+	return newSpring
+end
+
+local function useSpring(props: Spring.SpringProperties)
+	local controller = useRef()
+	local spring = controller.current
+
+	local binding, update = createBinding(props.start)
+
+	if not spring then
+		local newController = makeSpring(props, update)
+		spring = {
+			start = function(subProps: Spring.SpringProperties)
+				newController.props.target = subProps.target or newController.props.target
+				newController.props.speed = subProps.speed or newController.props.speed
+				newController.props.damper = subProps.damper or newController.props.damper
+				newController:Play(subProps.start or binding:getValue(), subProps.force)
+			end,
+
+			stop = function()
+				newController:Stop()
+			end,
+		}
+
+		controller.current = spring
+	end
+
+	return binding, spring.start, spring.stop
+end
+
+return useSpring
diff --git a/src/Hooks/useTween.lua b/src/Hooks/useTween.lua
new file mode 100644
index 0000000..dedf502
--- /dev/null
+++ b/src/Hooks/useTween.lua
@@ -0,0 +1,44 @@
+local React = require(script.Parent.Parent.React)
+local useRef = React.useRef
+local createBinding = React.createBinding
+
+local Tween = require(script.Parent.Parent.Animations.Types.Tween)
+
+local function makeTween<T>(props: Tween.TweenProperties<T>, updater: (any) -> nil)
+	local newSpring = Tween.new(props)
+	newSpring:SetListener(updater)
+
+	return newSpring
+end
+
+local function useTween<T>(props: Tween.TweenProperties<T>)
+	local controller = useRef()
+	local tween = controller.current
+
+	local binding, update = createBinding(props.start)
+
+	if not tween then
+		local newController = makeTween(props, update)
+
+		tween = {
+			start = function(subProps: Tween.TweenProperties<T>)
+				newController.props.info = subProps.info or newController.props.info
+				newController.props.start = subProps.start or newController.props.start
+				newController.props.target = subProps.target or newController.props.target
+				newController.props.immediate = subProps.immediate or newController.props.immediate
+				newController.props.delay = subProps.delay or newController.props.delay
+				newController:Play(subProps.start or binding:getValue())
+			end,
+
+			stop = function()
+				newController:Stop()
+			end,
+		}
+
+		controller.current = tween
+	end
+
+	return binding, tween.start, tween.stop
+end
+
+return useTween

From 47891268a14f5e5ae4c04a6704df692fd86a41e0 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Thu, 6 Jul 2023 15:20:13 -0400
Subject: [PATCH 07/22] Fix types, add impulse

---
 src/Animations/Types/Spring.lua | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/Animations/Types/Spring.lua b/src/Animations/Types/Spring.lua
index aa0d828..6b16676 100644
--- a/src/Animations/Types/Spring.lua
+++ b/src/Animations/Types/Spring.lua
@@ -11,8 +11,8 @@ export type Spring = typeof(Spring.new())
 export type SpringProperties = {
 	damper: number?,
 	speed: number?,
-	from: any,
-	to: any,
+	start: any,
+	target: any,
 }
 
 local Springs = {} :: {
@@ -31,7 +31,7 @@ function Spring.new(props: SpringProperties)
 	return self
 end
 
-function Spring:Play(from: any?)
+function Spring:Play(from: any?, force: Vector3?)
 	if self.playing then
 		self:Stop()
 	end
@@ -50,6 +50,10 @@ function Spring:Play(from: any?)
 		local newSpring = SpringValue.new(baseFromValue, self.props.speed, self.props.damper)
 
 		newSpring:SetGoal(baseToValue)
+		if force then
+			newSpring:Impulse(force)
+		end
+
 		Springs[newSpring] = {
 			resolve = resolve,
 			update = function()

From 799ea68e914fd46dfc78bbd81b49f2937abc414d Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Thu, 6 Jul 2023 15:20:33 -0400
Subject: [PATCH 08/22] Add hook for listening to bindings

---
 src/Hooks/useBindings.lua | 45 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)
 create mode 100644 src/Hooks/useBindings.lua

diff --git a/src/Hooks/useBindings.lua b/src/Hooks/useBindings.lua
new file mode 100644
index 0000000..e1b2950
--- /dev/null
+++ b/src/Hooks/useBindings.lua
@@ -0,0 +1,45 @@
+local React = require(script.Parent.Parent.React)
+local useEffect = React.useEffect
+-- TODO: Remove this when we have a better way to subscribe to bindings
+local subscribeToBinding = React.__subscribeToBinding
+
+local function useBindings(callback: (any...) -> (), bindings: { any }, deps: { table })
+	useEffect(function()
+		local disconnects = {}
+		local values = {}
+
+		local running = true
+
+		for i, binding in bindings do
+			values[i] = binding:getValue()
+		end
+
+		for i, binding in bindings do
+			local idx = i
+
+			disconnects[idx] = subscribeToBinding(binding, function(newValue)
+				if not running then
+					warn("Binding updated after unmount")
+					return
+				end
+
+				values[idx] = newValue
+				callback(unpack(values))
+			end)
+		end
+
+		callback(unpack(values))
+
+		return function()
+			for _, disconnect in disconnects do
+				disconnect()
+			end
+
+			running = false
+			table.clear(disconnects)
+			table.clear(values)
+		end
+	end, { unpack(bindings), unpack(deps or {}) })
+end
+
+return useBindings

From ede10d755c218638f7113679119f2ff1a99f4ed6 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Thu, 6 Jul 2023 15:20:42 -0400
Subject: [PATCH 09/22] Fix tween types

---
 src/Animations/Types/Tween.lua | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/Animations/Types/Tween.lua b/src/Animations/Types/Tween.lua
index 01c22d0..ea05b45 100644
--- a/src/Animations/Types/Tween.lua
+++ b/src/Animations/Types/Tween.lua
@@ -8,9 +8,12 @@ local Tween = {}
 Tween.__index = Tween
 
 export type Tween = typeof(Tween.new())
-export type TweenProperties = {
-	from: any,
-	to: any,
+export type TweenProperties<T> = {
+	info: TweenInfo,
+	start: T,
+	target: T,
+	immediate: boolean?,
+	delay: number?,
 }
 
 local function playTween(tweenInfo, callback: (number) -> nil, completed: () -> nil)
@@ -37,7 +40,7 @@ local function playTween(tweenInfo, callback: (number) -> nil, completed: () ->
 	end
 end
 
-function Tween.new(props: TweenProperties)
+function Tween.new<T>(props: TweenProperties<T>)
 	local self = setmetatable(BaseAnimation.new(), Tween) :: Tween
 
 	self.props = props

From 0666adf8df1104bc4c21e09190f783a980bd94f4 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Thu, 6 Jul 2023 15:20:47 -0400
Subject: [PATCH 10/22] Export new hooks

---
 src/init.lua | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/init.lua b/src/init.lua
index a06edcc..c6f9575 100644
--- a/src/init.lua
+++ b/src/init.lua
@@ -7,4 +7,9 @@ return {
 	useAnimation = require(script.Hooks.useAnimation),
 	useGroupAnimation = require(script.Hooks.useGroupAnimation),
 	useSequenceAnimation = require(script.Hooks.useSequenceAnimation),
+
+	useSpring = require(script.Hooks.useSpring),
+	useTween = require(script.Hooks.useTween),
+
+	useBindings = require(script.Hooks.useBindings),
 }

From 89678f119fa613d4342fbe48be6ab333455e3694 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Thu, 6 Jul 2023 15:20:53 -0400
Subject: [PATCH 11/22] Update test script

---
 test/Test.lua | 124 ++++++++++++++++++++++++++++----------------------
 1 file changed, 70 insertions(+), 54 deletions(-)

diff --git a/test/Test.lua b/test/Test.lua
index 69a41e2..5bf5bb5 100644
--- a/test/Test.lua
+++ b/test/Test.lua
@@ -8,7 +8,10 @@ local createElement = React.createElement
 local ReactAnimation = require(Packages.ReactAnimation)
 local useGroupAnimation = ReactAnimation.useGroupAnimation
 local useSequenceAnimation = ReactAnimation.useSequenceAnimation
-local useAnimation = ReactAnimation.useAnimation
+local useSpring = ReactAnimation.useSpring
+local useTween = ReactAnimation.useTween
+local useBindings = ReactAnimation.useBindings
+-- local useAnimation = ReactAnimation.useAnimation
 -- local Spring = ReactAnimation.Spring
 local Tween = ReactAnimation.Tween
 
@@ -119,58 +122,71 @@ local function TestSequence()
 end
 
 -- -- @TestTween
--- local function TestTween()
--- 	local value, update = useTween(UDim2.fromScale(0, 0), TweenInfo.new(1))
--- 	local value2, update2 = useTween(Color3.new(), TweenInfo.new(1))
-
--- 	if value.X.Scale == 0 then
--- 		update({ goal = UDim2.fromScale(1, 0) })
--- 	elseif (value :: UDim2).X.Scale == 1 then
--- 		update({ goal = UDim2.fromScale(0, 0) })
--- 	end
-
--- 	if value2.R == 0 then
--- 		update2({ start = Color3.new(1, 0, 0), goal = Color3.new(1, 0, 0) })
--- 	elseif value2.R == 1 then
--- 		update2({ goal = Color3.new(0, 0, 0) })
--- 	end
-
--- 	return createFrame({
--- 		Name = "Tween",
--- 		Position = value,
--- 		BackgroundColor3 = value2,
--- 	})
--- end
+local function TestTween()
+	local value, update = useTween({
+		info = TweenInfo.new(1),
+		start = UDim2.fromScale(0, 0),
+	})
+
+	local value2, update2 = useTween({
+		info = TweenInfo.new(1),
+		start = Color3.new(),
+	})
+
+	useBindings(function(v, v2)
+		if v.X.Scale == 0 then
+			update({ target = UDim2.fromScale(1, 0) })
+		elseif (v :: UDim2).X.Scale == 1 then
+			update({ target = UDim2.fromScale(0, 0) })
+		end
+
+		if v2.R == 0 then
+			update2({ start = Color3.new(1, 0, 0), target = Color3.new(1, 0, 0) })
+		elseif v2.R == 1 then
+			update2({ target = Color3.new(0, 0, 0) })
+		end
+	end, { value, value2 })
+
+	return createFrame({
+		Name = "Tween",
+		Position = value,
+		BackgroundColor3 = value2,
+	})
+end
 
 -- -- @TestSpring
--- local function TestSpring()
--- 	local value, update = useSpring(UDim2.fromScale(-1, 0), 10, 0.5)
-
--- 	useEffect(function()
--- 		local running = true
-
--- 		task.spawn(function()
--- 			while running do
--- 				update({ goal = UDim2.fromScale(1, 0) })
--- 				task.wait(3)
-
--- 				if running then
--- 					update({ goal = UDim2.fromScale(-1, 0) })
--- 					task.wait(3)
--- 				end
--- 			end
--- 		end)
-
--- 		return function()
--- 			running = false
--- 		end
--- 	end, {})
-
--- 	return createFrame({
--- 		Name = "Spring",
--- 		Position = value,
--- 	})
--- end
+local function TestSpring()
+	local value, update = useSpring({
+		start = UDim2.fromScale(0, 0),
+		speed = 5,
+		damper = 0.7,
+	})
+
+	useEffect(function()
+		local running = true
+
+		task.spawn(function()
+			while running do
+				update({ target = UDim2.fromScale(1, 0) })
+				task.wait(3)
+
+				if running then
+					update({ target = UDim2.fromScale(-1, 0) })
+					task.wait(3)
+				end
+			end
+		end)
+
+		return function()
+			running = false
+		end
+	end, {})
+
+	return createFrame({
+		Name = "Spring",
+		Position = value,
+	})
+end
 
 -- @entry
 local function Test()
@@ -183,9 +199,9 @@ local function Test()
 			VerticalAlignment = Enum.VerticalAlignment.Center,
 		}),
 
-		-- tween = createElement(TestTween),
-		-- spring = createElement(TestSpring),
-		sequence = createElement(TestSequence),
+		tween = createElement(TestTween),
+		spring = createElement(TestSpring),
+		-- sequence = createElement(TestSequence),
 	})
 end
 

From 9b9acf6827d20de0fc2552a87024d388d1885c64 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Fri, 7 Jul 2023 13:38:43 -0400
Subject: [PATCH 12/22] Added promise cancel

---
 src/Utility/SpringValue.lua | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/Utility/SpringValue.lua b/src/Utility/SpringValue.lua
index 402fd7f..c4ca39b 100644
--- a/src/Utility/SpringValue.lua
+++ b/src/Utility/SpringValue.lua
@@ -71,6 +71,7 @@ function SpringValue:Update(dt: number)
 	end
 
 	self._current = LinearValue.new(self._current._ccstr, unpack(newValues))
+
 	return updated
 end
 
@@ -83,7 +84,11 @@ function SpringValue:Stop()
 end
 
 function SpringValue:Run(update: () -> ())
-	return Promise.new(function(resolve)
+	return Promise.new(function(resolve, _, onCancel)
+		onCancel(function()
+			self:Stop()
+		end)
+
 		update(self:GetValue())
 		SpringValues[self] = { update, resolve }
 	end)

From d5c63f43c27d12dbc5eec487a71627dcbfc90c15 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Fri, 7 Jul 2023 13:38:58 -0400
Subject: [PATCH 13/22] Use SpringValue as controller

---
 src/Animations/Types/Spring.lua | 49 ++++++---------------------------
 1 file changed, 9 insertions(+), 40 deletions(-)

diff --git a/src/Animations/Types/Spring.lua b/src/Animations/Types/Spring.lua
index 6b16676..fa425b0 100644
--- a/src/Animations/Types/Spring.lua
+++ b/src/Animations/Types/Spring.lua
@@ -1,5 +1,3 @@
-local RunService = game:GetService("RunService")
-
 local BaseAnimation = require(script.Parent.Parent.Base)
 local Promise = require(script.Parent.Parent.Parent.Promise)
 local SpringValue = require(script.Parent.Parent.Parent.Utility.SpringValue)
@@ -15,13 +13,6 @@ export type SpringProperties = {
 	target: any,
 }
 
-local Springs = {} :: {
-	[Spring]: {
-		completed: () -> nil,
-		resolve: () -> (),
-	},
-}
-
 function Spring.new(props: SpringProperties)
 	local self = setmetatable(BaseAnimation.new(), Spring) :: Spring
 
@@ -46,27 +37,17 @@ function Spring:Play(from: any?, force: Vector3?)
 		return Promise.resolve()
 	end
 
-	local animation = Promise.new(function(resolve, _, onCancel)
-		local newSpring = SpringValue.new(baseFromValue, self.props.speed, self.props.damper)
+	local newSpring = SpringValue.new(baseFromValue, self.props.speed, self.props.damper)
+	newSpring:SetGoal(baseToValue)
 
-		newSpring:SetGoal(baseToValue)
-		if force then
-			newSpring:Impulse(force)
-		end
+	if force then
+		newSpring:Impulse(force)
+	end
 
-		Springs[newSpring] = {
-			resolve = resolve,
-			update = function()
-				if self.listener then
-					self.listener(newSpring:GetValue())
-				end
-			end,
-		}
-
-		onCancel(function()
-			Springs[newSpring] = nil
-			resolve()
-		end)
+	local animation = newSpring:Run(function()
+		if self.listener then
+			self.listener(newSpring:GetValue())
+		end
 	end)
 
 	self.playing = true
@@ -88,16 +69,4 @@ function Spring:Stop()
 	self.playing = false
 end
 
-RunService:BindToRenderStep("UPDATE_REACT_SPRINGS", Enum.RenderPriority.First.Value, function(dt: number)
-	for spring, data in Springs do
-		local didUpdate = spring:Update(dt)
-		data.update()
-
-		if not didUpdate then
-			Springs[spring] = nil
-			data.resolve()
-		end
-	end
-end)
-
 return Spring

From cbcb5292898001230f3104bc0c2d4e6064701da7 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Fri, 7 Jul 2023 13:50:54 -0400
Subject: [PATCH 14/22] Fix issue with listener

---
 src/Hooks/useSpring.lua | 14 ++++++--------
 src/Hooks/useTween.lua  | 13 +++++--------
 2 files changed, 11 insertions(+), 16 deletions(-)

diff --git a/src/Hooks/useSpring.lua b/src/Hooks/useSpring.lua
index a72ab06..7adbc6e 100644
--- a/src/Hooks/useSpring.lua
+++ b/src/Hooks/useSpring.lua
@@ -4,13 +4,6 @@ local createBinding = React.createBinding
 
 local Spring = require(script.Parent.Parent.Animations.Types.Spring)
 
-local function makeSpring(props: Spring.SpringProperties, updater: (any) -> nil)
-	local newSpring = Spring.new(props)
-	newSpring:SetListener(updater)
-
-	return newSpring
-end
-
 local function useSpring(props: Spring.SpringProperties)
 	local controller = useRef()
 	local spring = controller.current
@@ -18,8 +11,11 @@ local function useSpring(props: Spring.SpringProperties)
 	local binding, update = createBinding(props.start)
 
 	if not spring then
-		local newController = makeSpring(props, update)
+		local newController = Spring.new(props)
+
 		spring = {
+			controller = newController,
+
 			start = function(subProps: Spring.SpringProperties)
 				newController.props.target = subProps.target or newController.props.target
 				newController.props.speed = subProps.speed or newController.props.speed
@@ -35,6 +31,8 @@ local function useSpring(props: Spring.SpringProperties)
 		controller.current = spring
 	end
 
+	spring.controller:SetListener(update)
+
 	return binding, spring.start, spring.stop
 end
 
diff --git a/src/Hooks/useTween.lua b/src/Hooks/useTween.lua
index dedf502..882857e 100644
--- a/src/Hooks/useTween.lua
+++ b/src/Hooks/useTween.lua
@@ -4,13 +4,6 @@ local createBinding = React.createBinding
 
 local Tween = require(script.Parent.Parent.Animations.Types.Tween)
 
-local function makeTween<T>(props: Tween.TweenProperties<T>, updater: (any) -> nil)
-	local newSpring = Tween.new(props)
-	newSpring:SetListener(updater)
-
-	return newSpring
-end
-
 local function useTween<T>(props: Tween.TweenProperties<T>)
 	local controller = useRef()
 	local tween = controller.current
@@ -18,9 +11,11 @@ local function useTween<T>(props: Tween.TweenProperties<T>)
 	local binding, update = createBinding(props.start)
 
 	if not tween then
-		local newController = makeTween(props, update)
+		local newController = Tween.new(props)
 
 		tween = {
+			controller = newController,
+
 			start = function(subProps: Tween.TweenProperties<T>)
 				newController.props.info = subProps.info or newController.props.info
 				newController.props.start = subProps.start or newController.props.start
@@ -38,6 +33,8 @@ local function useTween<T>(props: Tween.TweenProperties<T>)
 		controller.current = tween
 	end
 
+	tween.controller:SetListener(update)
+
 	return binding, tween.start, tween.stop
 end
 

From e6b1ec21af0980e49c8da99fc5c68062c1a0dce6 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Fri, 7 Jul 2023 14:19:52 -0400
Subject: [PATCH 15/22] Update useSpring to use base controller

---
 src/Hooks/useSpring.lua     | 38 +++++++++++++++++++++++++++-------
 src/Utility/SpringValue.lua | 41 +++++++++++++++++++++++++++++++------
 2 files changed, 66 insertions(+), 13 deletions(-)

diff --git a/src/Hooks/useSpring.lua b/src/Hooks/useSpring.lua
index 7adbc6e..88f7895 100644
--- a/src/Hooks/useSpring.lua
+++ b/src/Hooks/useSpring.lua
@@ -2,6 +2,7 @@ local React = require(script.Parent.Parent.React)
 local useRef = React.useRef
 local createBinding = React.createBinding
 
+local SpringValue = require(script.Parent.Parent.Utility.SpringValue)
 local Spring = require(script.Parent.Parent.Animations.Types.Spring)
 
 local function useSpring(props: Spring.SpringProperties)
@@ -11,27 +12,50 @@ local function useSpring(props: Spring.SpringProperties)
 	local binding, update = createBinding(props.start)
 
 	if not spring then
-		local newController = Spring.new(props)
+		local newController = SpringValue.new(props.start, props.speed, props.damper)
 
 		spring = {
 			controller = newController,
 
 			start = function(subProps: Spring.SpringProperties)
-				newController.props.target = subProps.target or newController.props.target
-				newController.props.speed = subProps.speed or newController.props.speed
-				newController.props.damper = subProps.damper or newController.props.damper
-				newController:Play(subProps.start or binding:getValue(), subProps.force)
+				if subProps.target then
+					newController:SetGoal(subProps.target)
+				end
+
+				if subProps.start then
+					newController:SetValue(subProps.start)
+				end
+
+				if subProps.force then
+					newController:Impulse(subProps.force)
+				end
+
+				if subProps.damper then
+					newController:SetDamper(subProps.damper)
+				end
+
+				if subProps.speed then
+					newController:SetSpeed(subProps.speed)
+				end
+
+				if subProps.target or subProps.start or subProps.force then
+					if not newController:Playing() then
+						newController:Run()
+					end
+				end
 			end,
 
 			stop = function()
-				newController:Stop()
+				if newController:Playing() then
+					newController:Stop()
+				end
 			end,
 		}
 
 		controller.current = spring
 	end
 
-	spring.controller:SetListener(update)
+	spring.controller:SetUpdater(update)
 
 	return binding, spring.start, spring.stop
 end
diff --git a/src/Utility/SpringValue.lua b/src/Utility/SpringValue.lua
index c4ca39b..a12ef46 100644
--- a/src/Utility/SpringValue.lua
+++ b/src/Utility/SpringValue.lua
@@ -18,6 +18,7 @@ function SpringValue.new(initial: LinearValue.LinearValueType, speed: number?, d
 		_velocities = {},
 		_speed = speed or 1,
 		_damper = damper or 1,
+		_updater = nil,
 	}, SpringValue)
 end
 
@@ -37,6 +38,22 @@ function SpringValue:SetGoal(goal: LinearValue.LinearValueType)
 	self._goal = LinearValue.fromValue(goal)
 end
 
+function SpringValue:SetSpeed(speed: number)
+	self._speed = speed
+end
+
+function SpringValue:SetDamper(damper: number)
+	self._damper = damper
+end
+
+function SpringValue:SetUpdater(updater: (any) -> ())
+	self._updater = updater
+
+	if self:Playing() and updater then
+		updater(self:GetValue())
+	end
+end
+
 function SpringValue:GetGoal()
 	return self._goal:ToValue()
 end
@@ -75,6 +92,10 @@ function SpringValue:Update(dt: number)
 	return updated
 end
 
+function SpringValue:Playing()
+	return SpringValues[self] ~= nil
+end
+
 function SpringValue:Stop()
 	local value = SpringValues[self]
 	if value then
@@ -83,14 +104,21 @@ function SpringValue:Stop()
 	end
 end
 
-function SpringValue:Run(update: () -> ())
+function SpringValue:Run(update: () -> ()?)
+	if update then
+		self._updater = update
+	end
+
 	return Promise.new(function(resolve, _, onCancel)
 		onCancel(function()
 			self:Stop()
 		end)
 
-		update(self:GetValue())
-		SpringValues[self] = { update, resolve }
+		if update then
+			update(self:GetValue())
+		end
+
+		SpringValues[self] = resolve
 	end)
 end
 
@@ -135,12 +163,13 @@ end
 
 RunService:UnbindFromRenderStep("UPDATE_SPRING_VALUES")
 RunService:BindToRenderStep("UPDATE_SPRING_VALUES", Enum.RenderPriority.First.Value, function(dt: number)
-	for spring, updates in pairs(SpringValues) do
+	for spring, resolve in pairs(SpringValues) do
 		local didUpdate = spring:Update(dt)
 		local value = spring:GetValue()
 
-		local update, resolve = unpack(updates)
-		update(value)
+		if spring._updater then
+			spring._updater(value)
+		end
 
 		if not didUpdate then
 			SpringValues[spring] = nil

From 0a586726860c21fe7bb21d39c1db1cc789823426 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Tue, 11 Jul 2023 00:15:21 -0400
Subject: [PATCH 16/22] Updated createBinding -> useBinding

---
 src/Hooks/useSpring.lua | 4 ++--
 src/Hooks/useTween.lua  | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/Hooks/useSpring.lua b/src/Hooks/useSpring.lua
index 88f7895..b69a2d3 100644
--- a/src/Hooks/useSpring.lua
+++ b/src/Hooks/useSpring.lua
@@ -1,6 +1,6 @@
 local React = require(script.Parent.Parent.React)
 local useRef = React.useRef
-local createBinding = React.createBinding
+local useBinding = React.useBinding
 
 local SpringValue = require(script.Parent.Parent.Utility.SpringValue)
 local Spring = require(script.Parent.Parent.Animations.Types.Spring)
@@ -9,7 +9,7 @@ local function useSpring(props: Spring.SpringProperties)
 	local controller = useRef()
 	local spring = controller.current
 
-	local binding, update = createBinding(props.start)
+	local binding, update = useBinding(props.start)
 
 	if not spring then
 		local newController = SpringValue.new(props.start, props.speed, props.damper)
diff --git a/src/Hooks/useTween.lua b/src/Hooks/useTween.lua
index 882857e..6cc791f 100644
--- a/src/Hooks/useTween.lua
+++ b/src/Hooks/useTween.lua
@@ -1,6 +1,6 @@
 local React = require(script.Parent.Parent.React)
 local useRef = React.useRef
-local createBinding = React.createBinding
+local useBinding = React.useBinding
 
 local Tween = require(script.Parent.Parent.Animations.Types.Tween)
 
@@ -8,7 +8,7 @@ local function useTween<T>(props: Tween.TweenProperties<T>)
 	local controller = useRef()
 	local tween = controller.current
 
-	local binding, update = createBinding(props.start)
+	local binding, update = useBinding(props.start)
 
 	if not tween then
 		local newController = Tween.new(props)

From 86c28426cba63a517793b23a00971b0e30cd7fc4 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Tue, 11 Jul 2023 01:38:55 -0400
Subject: [PATCH 17/22] Fix tween

---
 src/Animations/Types/Tween.lua | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Animations/Types/Tween.lua b/src/Animations/Types/Tween.lua
index ea05b45..ae43043 100644
--- a/src/Animations/Types/Tween.lua
+++ b/src/Animations/Types/Tween.lua
@@ -55,7 +55,7 @@ function Tween:Play(from: any?)
 	end
 
 	local tweenInfo = self.props.info :: TweenInfo
-	local baseFromValue = from or self.props.start :: any
+	local baseFromValue = self.props.start or from :: any
 	local baseToValue = self.props.target :: any
 
 	-- start immediately will start the tween but not update the listener
@@ -78,7 +78,7 @@ function Tween:Play(from: any?)
 	assert(tweenInfo.Reverses == false, "Reverses must be false")
 	assert(tweenInfo.DelayTime == 0, "DelayTime must be 0")
 
-	if from == baseToValue then
+	if baseFromValue == baseToValue then
 		return Promise.resolve()
 	end
 

From 901887648919786013a3628e1f6913c48d8bb859 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Tue, 11 Jul 2023 21:28:29 -0400
Subject: [PATCH 18/22] Spring fixes, fix terms to be more accurate

---
 src/Animations/Types/Spring.lua | 16 +++++++++++-----
 src/Animations/Types/Tween.lua  |  4 ++--
 src/Utility/SpringValue.lua     |  6 +++++-
 3 files changed, 18 insertions(+), 8 deletions(-)

diff --git a/src/Animations/Types/Spring.lua b/src/Animations/Types/Spring.lua
index fa425b0..288c475 100644
--- a/src/Animations/Types/Spring.lua
+++ b/src/Animations/Types/Spring.lua
@@ -22,24 +22,29 @@ function Spring.new(props: SpringProperties)
 	return self
 end
 
-function Spring:Play(from: any?, force: Vector3?)
+function Spring:Play(from: any?)
 	if self.playing then
 		self:Stop()
 	end
 
-	local baseFromValue = from or self.props.start :: any
+	local baseFromValue = self.props.start or from :: any
 	local baseToValue = self.props.target :: any
+	local force = self.props.force :: any
 
-	assert(baseFromValue, "No from value provided")
-	assert(baseToValue, "No to value provided")
+	assert(baseFromValue, "No start value provided")
+	assert(baseToValue, "No target value provided")
 
-	if from == baseToValue then
+	if baseFromValue == baseToValue and not force then
 		return Promise.resolve()
 	end
 
 	local newSpring = SpringValue.new(baseFromValue, self.props.speed, self.props.damper)
 	newSpring:SetGoal(baseToValue)
 
+	if self._oldSpring then
+		newSpring:Impulse(self._oldSpring:GetVelocity())
+	end
+
 	if force then
 		newSpring:Impulse(force)
 	end
@@ -52,6 +57,7 @@ function Spring:Play(from: any?, force: Vector3?)
 
 	self.playing = true
 	self.player = animation
+	self._oldSpring = newSpring
 
 	return animation
 end
diff --git a/src/Animations/Types/Tween.lua b/src/Animations/Types/Tween.lua
index ae43043..b53a0e2 100644
--- a/src/Animations/Types/Tween.lua
+++ b/src/Animations/Types/Tween.lua
@@ -70,8 +70,8 @@ function Tween:Play(from: any?)
 		end
 	end
 
-	assert(baseFromValue, "No from value provided")
-	assert(baseToValue, "No to value provided")
+	assert(baseFromValue, "No start value provided")
+	assert(baseToValue, "No target value provided")
 
 	assert(tweenInfo, "No tween info provided")
 	assert(tweenInfo.RepeatCount == 0, "RepeatCount must be 0")
diff --git a/src/Utility/SpringValue.lua b/src/Utility/SpringValue.lua
index a12ef46..df78aef 100644
--- a/src/Utility/SpringValue.lua
+++ b/src/Utility/SpringValue.lua
@@ -34,6 +34,10 @@ function SpringValue:Impulse(impulse: LinearValue.LinearValueType)
 	end
 end
 
+function SpringValue:GetVelocity()
+	return LinearValue.new(self._current._ccstr, unpack(self._velocities)):ToValue()
+end
+
 function SpringValue:SetGoal(goal: LinearValue.LinearValueType)
 	self._goal = LinearValue.fromValue(goal)
 end
@@ -100,7 +104,7 @@ function SpringValue:Stop()
 	local value = SpringValues[self]
 	if value then
 		SpringValues[self] = nil
-		value[2]()
+		value()
 	end
 end
 

From 2571901f8bda772d5772ce5c642382723aad7073 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Tue, 11 Jul 2023 21:50:12 -0400
Subject: [PATCH 19/22] Fix sequence overriding

---
 src/Hooks/useSequenceAnimation.lua | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/Hooks/useSequenceAnimation.lua b/src/Hooks/useSequenceAnimation.lua
index 532fd2f..be257e4 100644
--- a/src/Hooks/useSequenceAnimation.lua
+++ b/src/Hooks/useSequenceAnimation.lua
@@ -56,6 +56,7 @@ function Sequence:Play(fromProps: SequenceProps)
 
 	local animation = Promise.new(function(resolve, _, onCancel)
 		local promises = {}
+		local playing = {}
 
 		for _, sequenced in self.animation do
 			local animationPromise = Promise.delay(sequenced.timestamp):andThen(function()
@@ -66,7 +67,14 @@ function Sequence:Play(fromProps: SequenceProps)
 						continue
 					end
 
-					table.insert(allAnimatables, animatable:Play(fromProps[name]))
+					if playing[name] then
+						playing[name]:cancel()
+					end
+
+					local promise = animatable:Play(fromProps[name])
+					playing[name] = promise
+
+					table.insert(allAnimatables, promise)
 				end
 
 				return Promise.all(allAnimatables)

From 858a69baafd488648b5f9c601b6bc0446ce1b79f Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Tue, 11 Jul 2023 22:36:54 -0400
Subject: [PATCH 20/22] Update tween immediate

---
 src/Animations/Types/Tween.lua | 27 +++++++++------------------
 1 file changed, 9 insertions(+), 18 deletions(-)

diff --git a/src/Animations/Types/Tween.lua b/src/Animations/Types/Tween.lua
index b53a0e2..251a5e1 100644
--- a/src/Animations/Types/Tween.lua
+++ b/src/Animations/Types/Tween.lua
@@ -10,9 +10,9 @@ Tween.__index = Tween
 export type Tween = typeof(Tween.new())
 export type TweenProperties<T> = {
 	info: TweenInfo,
+	startImmediate: T?,
 	start: T,
 	target: T,
-	immediate: boolean?,
 	delay: number?,
 }
 
@@ -33,6 +33,7 @@ local function playTween(tweenInfo, callback: (number) -> nil, completed: () ->
 	end)
 
 	return function()
+		callback(0)
 		tween:Play()
 	end, function()
 		numberValue:Destroy()
@@ -55,18 +56,18 @@ function Tween:Play(from: any?)
 	end
 
 	local tweenInfo = self.props.info :: TweenInfo
-	local baseFromValue = self.props.start or from :: any
+	local baseFromValue = self.props.startImmediate or self.props.start or from :: any
 	local baseToValue = self.props.target :: any
 
 	-- start immediately will start the tween but not update the listener
-	local startImmediately = self.props.immediate == true
+	local startImmediately = self.props.startImmediate ~= nil
 	local delayTime = self.props.delay :: number
 
 	if not delayTime then
 		assert(startImmediately == false, "Cannot start immediately without a delay")
 	else
 		if startImmediately then
-			assert(delayTime < tweenInfo.Time, "DelayTime must be less than Time")
+			assert(delayTime > 0, "DelayTime must be greater than zero")
 		end
 	end
 
@@ -86,13 +87,7 @@ function Tween:Play(from: any?)
 	local toValue = LinearValue.fromValue(baseToValue)
 
 	local animation = Promise.new(function(resolve, _, onCancel)
-		local canUpdate = true
-
 		local play, cancel = playTween(tweenInfo, function(value)
-			if not canUpdate then
-				return
-			end
-
 			local newValue = fromValue:Lerp(toValue, value):ToValue()
 			self.listener(newValue)
 		end, function()
@@ -107,15 +102,11 @@ function Tween:Play(from: any?)
 			play()
 		else
 			if startImmediately then
-				canUpdate = false
-				play()
-
-				task.wait(delayTime)
-				canUpdate = true
-			else
-				task.wait(delayTime)
-				play()
+				self.listener(baseFromValue)
 			end
+
+			task.wait(delayTime)
+			play()
 		end
 	end)
 

From 0c1c9173e71c3cd2510e146b8d0e6bce45f9894d Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Tue, 25 Jul 2023 22:01:06 -0400
Subject: [PATCH 21/22] Added type checks for update

---
 src/Hooks/useGroupAnimation.lua | 2 ++
 src/Hooks/useSpring.lua         | 2 ++
 src/Hooks/useTween.lua          | 2 ++
 3 files changed, 6 insertions(+)

diff --git a/src/Hooks/useGroupAnimation.lua b/src/Hooks/useGroupAnimation.lua
index d8f996e..49c169f 100644
--- a/src/Hooks/useGroupAnimation.lua
+++ b/src/Hooks/useGroupAnimation.lua
@@ -90,6 +90,8 @@ local function useGroupAnimation(props: GroupAnimation, default: DefaultProperti
 
 		current = {
 			play = function(newState: string)
+				assert(typeof(newState) == "string", "useGroupAnimation expects a string 'state'")
+
 				newController:Play(newState)
 			end,
 
diff --git a/src/Hooks/useSpring.lua b/src/Hooks/useSpring.lua
index b69a2d3..20779f4 100644
--- a/src/Hooks/useSpring.lua
+++ b/src/Hooks/useSpring.lua
@@ -18,6 +18,8 @@ local function useSpring(props: Spring.SpringProperties)
 			controller = newController,
 
 			start = function(subProps: Spring.SpringProperties)
+				assert(typeof(subProps) == "table", "useSpring expects a table of properties")
+
 				if subProps.target then
 					newController:SetGoal(subProps.target)
 				end
diff --git a/src/Hooks/useTween.lua b/src/Hooks/useTween.lua
index 6cc791f..9f65a22 100644
--- a/src/Hooks/useTween.lua
+++ b/src/Hooks/useTween.lua
@@ -17,6 +17,8 @@ local function useTween<T>(props: Tween.TweenProperties<T>)
 			controller = newController,
 
 			start = function(subProps: Tween.TweenProperties<T>)
+				assert(typeof(subProps) == "table", "useTween expects a table of properties")
+
 				newController.props.info = subProps.info or newController.props.info
 				newController.props.start = subProps.start or newController.props.start
 				newController.props.target = subProps.target or newController.props.target

From 49fe90b5e60e575615b34227b6b8b931a3388b71 Mon Sep 17 00:00:00 2001
From: Jack Fox <0xdeadbeef1@gmail.com>
Date: Tue, 25 Jul 2023 22:35:06 -0400
Subject: [PATCH 22/22] fix start value for tweens

---
 src/Hooks/useTween.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Hooks/useTween.lua b/src/Hooks/useTween.lua
index 9f65a22..17ee60d 100644
--- a/src/Hooks/useTween.lua
+++ b/src/Hooks/useTween.lua
@@ -20,7 +20,7 @@ local function useTween<T>(props: Tween.TweenProperties<T>)
 				assert(typeof(subProps) == "table", "useTween expects a table of properties")
 
 				newController.props.info = subProps.info or newController.props.info
-				newController.props.start = subProps.start or newController.props.start
+				newController.props.start = subProps.start or binding:getValue()
 				newController.props.target = subProps.target or newController.props.target
 				newController.props.immediate = subProps.immediate or newController.props.immediate
 				newController.props.delay = subProps.delay or newController.props.delay
