Files
zeekayandHanzo Dev 751ef5c778 rename the framework to @hanzo/gui and the loader to @hanzogui/loader
Both old names are owned by a different npm account (artemis-prime), so
publishing them returns 403 — which is how this surfaced: the 8.0.0 run
shipped @hanzogui/logo and @hanzogui/demos depending on [email protected], a
version that cannot be published and therefore does not exist. Those two
were uninstallable.

@hanzo/gui and @hanzogui/* are ours, so the framework now carries the name
it is imported by everywhere downstream, and the loader moves under the
scope we control.

The rewrite is limited to package references. `DEBUG?.startsWith('hanzogui')`
is a log-namespace filter and join(dir, 'hanzogui') is a path — rewriting
either would change behaviour, so both are untouched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-30 20:23:18 -07:00

155 lines
4.3 KiB
TypeScript

import { createContext, useContext, useEffect, useState } from 'react'
import type { GuiElement, ViewProps } from '@hanzo/gui'
import { isWeb, View, withStaticProperties } from '@hanzo/gui'
// 3D Card Context - shared hover state between container and items
const MouseEnterContext = createContext<
[boolean, React.Dispatch<React.SetStateAction<boolean>>] | undefined
>(undefined)
export const useMouseEnter = () => {
const context = useContext(MouseEnterContext)
if (context === undefined) {
if (!isWeb) {
return [false, () => {}] as [boolean, React.Dispatch<React.SetStateAction<boolean>>]
}
throw new Error('useMouseEnter must be used within a Card3D')
}
return context
}
export type Card3DContainerProps = ViewProps & {
/** Custom class name for the container */
className?: string
/** Tilt intensity - higher = less tilt. Default: 10 */
tiltDivisor?: number
/** Center content in container */
centered?: boolean
}
const Card3DContainer = ({
children,
className,
tiltDivisor = 10,
centered = false,
...rest
}: Card3DContainerProps) => {
const [_containerRef, setContainerRef] = useState<GuiElement | null>(null)
const [perspectiveRef, setPerspectiveRef] = useState<GuiElement | null>(null)
const [isMouseEntered, setIsMouseEntered] = useState(false)
const containerRef = _containerRef as unknown as HTMLDivElement
useEffect(() => {
if (perspectiveRef && isWeb) {
;(perspectiveRef as unknown as HTMLDivElement).style.perspective = '800px'
}
}, [perspectiveRef])
useEffect(() => {
if (containerRef && isWeb) {
containerRef.style.transformStyle = 'preserve-3d'
containerRef.style.transition = 'transform 100ms ease-out'
}
}, [containerRef])
let handleMouseMove: (e: React.MouseEvent<HTMLDivElement>) => void
let handleMouseEnter: () => void
let handleMouseLeave: () => void
if (isWeb) {
handleMouseMove = (e) => {
if (!containerRef) return
const { left, top, width, height } = containerRef.getBoundingClientRect()
const x = (e.clientX - left - width / 2) / tiltDivisor
const y = (e.clientY - top - height / 2) / tiltDivisor
containerRef.style.transform = `rotateY(${x}deg) rotateX(${-y}deg)`
}
handleMouseEnter = () => {
setIsMouseEntered(true)
}
handleMouseLeave = () => {
if (!containerRef) return
setIsMouseEntered(false)
containerRef.style.transform = 'rotateY(0deg) rotateX(0deg)'
}
}
return (
<MouseEnterContext.Provider value={[isMouseEntered, setIsMouseEntered]}>
<View
ref={(ref) => setPerspectiveRef(ref)}
className={className}
{...(centered && {
justify: 'center',
items: 'center',
})}
{...rest}
>
<View
ref={(ref) => setContainerRef(ref)}
{...(centered && {
justify: 'center',
items: 'center',
})}
{...(isWeb && {
onMouseEnter: handleMouseEnter!,
onMouseMove: handleMouseMove!,
onMouseLeave: handleMouseLeave!,
})}
>
{children}
</View>
</View>
</MouseEnterContext.Provider>
)
}
export type Card3DItemProps = {
/** Z-axis translation on hover in pixels */
translateZ?: number
}
const Card3DItem = View.styleable<Card3DItemProps>(
({ translateZ = 0, children, ...rest }, forwardedRef) => {
const [ref, setRef] = useState<GuiElement | null>(null)
const [isMouseEntered] = useMouseEnter()
useEffect(() => {
if (isWeb && ref) {
;(ref as HTMLElement).style.transition = 'transform 200ms ease-out'
}
}, [ref])
useEffect(() => {
if (!isWeb || !ref) return
if (isMouseEntered) {
;(ref as HTMLElement).style.transform = `translateZ(${translateZ}px)`
} else {
;(ref as HTMLElement).style.transform = 'translateZ(0px)'
}
}, [isMouseEntered, translateZ, ref])
return (
<View
ref={(r) => {
setRef(r)
if (typeof forwardedRef === 'function') forwardedRef(r)
else if (forwardedRef) forwardedRef.current = r
}}
{...rest}
>
{children}
</View>
)
}
)
export const Card3D = withStaticProperties(Card3DContainer, {
Item: Card3DItem,
})
export { MouseEnterContext }