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/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..288c475
--- /dev/null
+++ b/src/Animations/Types/Spring.lua
@@ -0,0 +1,78 @@
+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?,
+	start: any,
+	target: any,
+}
+
+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 = self.props.start or from :: any
+	local baseToValue = self.props.target :: any
+	local force = self.props.force :: any
+
+	assert(baseFromValue, "No start value provided")
+	assert(baseToValue, "No target value provided")
+
+	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
+
+	local animation = newSpring:Run(function()
+		if self.listener then
+			self.listener(newSpring:GetValue())
+		end
+	end)
+
+	self.playing = true
+	self.player = animation
+	self._oldSpring = newSpring
+
+	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
+
+return Spring
diff --git a/src/Animations/Types/Tween.lua b/src/Animations/Types/Tween.lua
new file mode 100644
index 0000000..251a5e1
--- /dev/null
+++ b/src/Animations/Types/Tween.lua
@@ -0,0 +1,132 @@
+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<T> = {
+	info: TweenInfo,
+	startImmediate: T?,
+	start: T,
+	target: T,
+	delay: number?,
+}
+
+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()
+		callback(0)
+		tween:Play()
+	end, function()
+		numberValue:Destroy()
+		tween:Cancel()
+	end
+end
+
+function Tween.new<T>(props: TweenProperties<T>)
+	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 = 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.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 > 0, "DelayTime must be greater than zero")
+		end
+	end
+
+	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")
+	assert(tweenInfo.Reverses == false, "Reverses must be false")
+	assert(tweenInfo.DelayTime == 0, "DelayTime must be 0")
+
+	if baseFromValue == baseToValue then
+		return Promise.resolve()
+	end
+
+	local fromValue = LinearValue.fromValue(baseFromValue)
+	local toValue = LinearValue.fromValue(baseToValue)
+
+	local animation = Promise.new(function(resolve, _, onCancel)
+		local play, cancel = playTween(tweenInfo, function(value)
+			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
+				self.listener(baseFromValue)
+			end
+
+			task.wait(delayTime)
+			play()
+		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..cc2d46c
--- /dev/null
+++ b/src/Hooks/useAnimation.lua
@@ -0,0 +1,90 @@
+local Promise = require(script.Parent.Parent.Promise)
+
+local React = require(script.Parent.Parent.React)
+local useRef = React.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/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
diff --git a/src/Hooks/useGroupAnimation.lua b/src/Hooks/useGroupAnimation.lua
new file mode 100644
index 0000000..49c169f
--- /dev/null
+++ b/src/Hooks/useGroupAnimation.lua
@@ -0,0 +1,109 @@
+local React = require(script.Parent.Parent.React)
+local useBinding = React.useBinding
+local useRef = React.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 binding, updateBinding = useBinding(value)
+
+		setters[name] = updateBinding
+		values[name] = binding
+	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)
+				assert(typeof(newState) == "string", "useGroupAnimation expects a string 'state'")
+
+				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..be257e4
--- /dev/null
+++ b/src/Hooks/useSequenceAnimation.lua
@@ -0,0 +1,128 @@
+local Promise = require(script.Parent.Parent.Promise)
+
+local React = require(script.Parent.Parent.React)
+local useRef = React.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 = {}
+		local playing = {}
+
+		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
+
+					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)
+			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
index e71df6c..20779f4 100644
--- a/src/Hooks/useSpring.lua
+++ b/src/Hooks/useSpring.lua
@@ -1,75 +1,65 @@
-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 React = require(script.Parent.Parent.React)
+local useRef = React.useRef
+local useBinding = React.useBinding
+
+local SpringValue = require(script.Parent.Parent.Utility.SpringValue)
+local Spring = require(script.Parent.Parent.Animations.Types.Spring)
+
+local function useSpring(props: Spring.SpringProperties)
+	local controller = useRef()
+	local spring = controller.current
+
+	local binding, update = useBinding(props.start)
+
+	if not spring then
+		local newController = SpringValue.new(props.start, props.speed, props.damper)
+
+		spring = {
+			controller = newController,
+
+			start = function(subProps: Spring.SpringProperties)
+				assert(typeof(subProps) == "table", "useSpring expects a table of properties")
 
-local function useSpring(initial: LinearValue.LinearValueType, speed: number?, dampening: number?)
-	speed = speed or 20
-	dampening = dampening or 1
+				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()
+				if newController:Playing() then
+					newController:Stop()
+				end
+			end,
+		}
+
+		controller.current = spring
+	end
 
-	local state, setState = useState(initial)
-	local spring = useMemo(function()
-		return makeSpring(setState, initial, speed, dampening)
-	end, { initial, speed, dampening })
+	spring.controller:SetUpdater(update)
 
-	return state, spring.play, spring.stop
+	return binding, spring.start, 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
index 5728ea1..17ee60d 100644
--- a/src/Hooks/useTween.lua
+++ b/src/Hooks/useTween.lua
@@ -1,89 +1,43 @@
-local TweenService = game:GetService("TweenService")
+local React = require(script.Parent.Parent.React)
+local useRef = React.useRef
+local useBinding = React.useBinding
 
-local Roact = require(script.Parent.Parent.Roact)
-local useRef = Roact.useRef
-local useState = Roact.useState
-local useEffect = Roact.useEffect
+local Tween = require(script.Parent.Parent.Animations.Types.Tween)
 
-local LinearValue = require(script.Parent.Parent.Utility.LinearValue)
-type LinearValue = typeof(LinearValue)
+local function useTween<T>(props: Tween.TweenProperties<T>)
+	local controller = useRef()
+	local tween = controller.current
 
-local function makeTween(updateState: (any) -> (), initial: LinearValue.LinearValueType, tweenInfo: TweenInfo)
-	local start, goal, tween
-	local current = LinearValue.fromValue(initial)
+	local binding, update = useBinding(props.start)
 
-	local value = Instance.new("NumberValue")
-	value:GetPropertyChangedSignal("Value"):Connect(function()
-		current = start:Lerp(goal, value.Value)
-		updateState(current:ToValue())
-	end)
+	if not tween then
+		local newController = Tween.new(props)
 
-	return {
-		destroy = function()
-			value:Destroy()
-		end,
+		tween = {
+			controller = newController,
 
-		play = function(animation: {
-			start: LinearValue.LinearValueType?,
-			goal: LinearValue.LinearValueType?,
-		})
-			local startValue = animation.start
-			local endValue = animation.goal
+			start = function(subProps: Tween.TweenProperties<T>)
+				assert(typeof(subProps) == "table", "useTween expects a table of properties")
 
-			local currentValue = current:ToValue()
+				newController.props.info = subProps.info or newController.props.info
+				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
+				newController:Play(subProps.start or binding:getValue())
+			end,
 
-			if startValue == currentValue and endValue == currentValue then
-				return
-			end
+			stop = function()
+				newController:Stop()
+			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
+		controller.current = tween
 	end
 
-	useEffect(function()
-		return function()
-			local currentTween = tweenRef.current
-			if currentTween then
-				currentTween:destroy()
-				tweenRef.current = nil
-			end
-		end
-	end, {})
+	tween.controller:SetListener(update)
 
-	return current, currentTweenRef.play, currentTweenRef.stop
+	return binding, tween.start, tween.stop
 end
 
 return useTween
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/src/Utility/SpringValue.lua b/src/Utility/SpringValue.lua
index 402fd7f..df78aef 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
 
@@ -33,10 +34,30 @@ 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
 
+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
@@ -71,21 +92,37 @@ function SpringValue:Update(dt: number)
 	end
 
 	self._current = LinearValue.new(self._current._ccstr, unpack(newValues))
+
 	return updated
 end
 
+function SpringValue:Playing()
+	return SpringValues[self] ~= nil
+end
+
 function SpringValue:Stop()
 	local value = SpringValues[self]
 	if value then
 		SpringValues[self] = nil
-		value[2]()
+		value()
 	end
 end
 
-function SpringValue:Run(update: () -> ())
-	return Promise.new(function(resolve)
-		update(self:GetValue())
-		SpringValues[self] = { update, resolve }
+function SpringValue:Run(update: () -> ()?)
+	if update then
+		self._updater = update
+	end
+
+	return Promise.new(function(resolve, _, onCancel)
+		onCancel(function()
+			self:Stop()
+		end)
+
+		if update then
+			update(self:GetValue())
+		end
+
+		SpringValues[self] = resolve
 	end)
 end
 
@@ -130,12 +167,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
diff --git a/src/init.lua b/src/init.lua
index bb303a8..c6f9575 100644
--- a/src/init.lua
+++ b/src/init.lua
@@ -1,8 +1,15 @@
+local Animations = require(script.Animations)
+
 return {
-	Animation = require(script.Animation),
+	Tween = Animations.Tween,
+	Spring = Animations.Spring,
+
+	useAnimation = require(script.Hooks.useAnimation),
+	useGroupAnimation = require(script.Hooks.useGroupAnimation),
+	useSequenceAnimation = require(script.Hooks.useSequenceAnimation),
 
-	useLatest = require(script.Hooks.useLatest),
-	useSequence = require(script.Hooks.useSequence),
 	useSpring = require(script.Hooks.useSpring),
 	useTween = require(script.Hooks.useTween),
+
+	useBindings = require(script.Hooks.useBindings),
 }
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 28f0fa4..5bf5bb5 100644
--- a/test/Test.lua
+++ b/test/Test.lua
@@ -1,25 +1,33 @@
 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 Animation = ReactAnimation.Animation
-local useTween = ReactAnimation.useTween
+local ReactAnimation = require(Packages.ReactAnimation)
+local useGroupAnimation = ReactAnimation.useGroupAnimation
+local useSequenceAnimation = ReactAnimation.useSequenceAnimation
 local useSpring = ReactAnimation.useSpring
-local useSequence = ReactAnimation.useSequence
-local useLatest = ReactAnimation.useLatest
+local useTween = ReactAnimation.useTween
+local useBindings = ReactAnimation.useBindings
+-- 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,91 +38,114 @@ 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) }
-		),
+	-- 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),
 	})
 
 	useEffect(function()
-		play()
+		local running = true
 
-		return stop
+		task.spawn(function()
+			while running do
+				play("yo")
+				task.wait(3)
+			end
+		end)
+
+		return function()
+			running = false
+			stop()
+		end
 	end, {})
 
 	return createFrame({
 		Name = "Sequence",
 		Position = sequence.position,
 		Size = sequence.size,
+		ParentSize = UDim2.fromScale(1, 0.25),
 	})
 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) }
-		),
+-- -- @TestTween
+local function TestTween()
+	local value, update = useTween({
+		info = TweenInfo.new(1),
+		start = UDim2.fromScale(0, 0),
 	})
 
-	useEffect(function()
-		play1()
-		play2()
-	end, {})
-
-	return createFrame({
-		Name = "Priority",
-		Position = useLatest(sequence1.position, sequence2.position),
-		Size = useLatest(sequence1.size, sequence2.size),
+	local value2, update2 = useTween({
+		info = TweenInfo.new(1),
+		start = Color3.new(),
 	})
-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
+	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 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
+		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",
@@ -123,20 +154,24 @@ local function TestTween()
 	})
 end
 
--- @TestSpring
+-- -- @TestSpring
 local function TestSpring()
-	local value, update = useSpring(UDim2.fromScale(-1, 0), 10, 0.5)
+	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({ goal = UDim2.fromScale(1, 0) })
+				update({ target = UDim2.fromScale(1, 0) })
 				task.wait(3)
 
 				if running then
-					update({ goal = UDim2.fromScale(-1, 0) })
+					update({ target = UDim2.fromScale(-1, 0) })
 					task.wait(3)
 				end
 			end
@@ -166,8 +201,7 @@ local function Test()
 
 		tween = createElement(TestTween),
 		spring = createElement(TestSpring),
-		sequence = createElement(TestSequence),
-		priority = createElement(TestPriority),
+		-- sequence = createElement(TestSequence),
 	})
 end
 
diff --git a/test/init.client.lua b/test/init.client.lua
index 13ceb54..73352c2 100644
--- a/test/init.client.lua
+++ b/test/init.client.lua
@@ -3,17 +3,18 @@ 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 ReactRoblox = require(Packages.ReactRoblox)
+local createElement = React.createElement
 
 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 ccb5cde..bc1b30f 100644
--- a/test/init.story.lua
+++ b/test/init.story.lua
@@ -1,19 +1,20 @@
 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 ReactRoblox = require(Packages.ReactRoblox)
+local createElement = React.createElement
 
 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)
+	local root = ReactRoblox.createRoot(container)
+	root:render(createElement(Test))
 
 	return function()
-		unmount(root)
-		roactAnimation.Parent = ReplicatedStorage
+		ReactAnimation.Parent = ReplicatedStorage
+		root:unmount()
 	end
 end
diff --git a/wally.toml b/wally.toml
index 768f184..d549419 100644
--- a/wally.toml
+++ b/wally.toml
@@ -1,10 +1,12 @@
 [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/react@17.0.1-rc.18"
+ReactRoblox = "core-packages/react-roblox@17.0.1-rc.16"
 Promise = "evaera/promise@4.0.0"
 
