Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | 'use client'
import { type CSSProperties, type HTMLAttributes, useEffect, useRef } from 'react'
import { type DockConfig, useMyAbacus } from '@/contexts/MyAbacusContext'
export interface AbacusDockProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
/** Optional identifier for debugging */
id?: string
/** Number of columns to display (default: 5) */
columns?: number
/** Whether the abacus is interactive (default: true) */
interactive?: boolean
/** Whether to show numbers below columns (default: true) */
showNumbers?: boolean
/** Whether to animate bead movements (default: true) */
animated?: boolean
/** Scale factor for the abacus (default: auto-fit to container) */
scaleFactor?: number
/** Controlled value - when provided, dock controls the abacus value */
value?: number
/** Default value for uncontrolled mode */
defaultValue?: number
/** Callback when value changes (for controlled mode) */
onValueChange?: (newValue: number) => void
/** Hide the undock button (default: false) */
hideUndock?: boolean
}
/**
* AbacusDock - A container that the global MyAbacus will render into
*
* Place this component anywhere you want the abacus to appear docked.
* When mounted, the global abacus will portal into this container instead
* of showing as a floating button.
*
* @example
* ```tsx
* // Basic usage - abacus will auto-fit to the container
* <AbacusDock className={css({ width: '300px', height: '400px' })} />
*
* // With custom configuration
* <AbacusDock
* columns={4}
* interactive={true}
* showNumbers={true}
* className={css({ width: '250px', height: '350px' })}
* />
* ```
*/
export function AbacusDock({
id,
columns = 5,
interactive = true,
showNumbers = true,
animated = true,
scaleFactor,
value,
defaultValue,
onValueChange,
hideUndock = false,
style,
...divProps
}: AbacusDockProps) {
const containerRef = useRef<HTMLDivElement>(null)
const { registerDock, unregisterDock, updateDockVisibility, updateDockConfig } = useMyAbacus()
// Register the dock on mount, unregister on unmount
useEffect(() => {
const element = containerRef.current
if (!element) return
const config: DockConfig = {
element,
id,
columns,
interactive,
showNumbers,
animated,
scaleFactor,
value,
defaultValue,
onValueChange,
hideUndock,
isVisible: false, // Will be updated by IntersectionObserver
}
registerDock(config)
return () => {
unregisterDock(element)
}
// Only register/unregister on mount/unmount - config updates happen separately
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [registerDock, unregisterDock])
// Update dock config when props change (without re-registering)
useEffect(() => {
const element = containerRef.current
if (!element) return
updateDockConfig(element, {
id,
columns,
interactive,
showNumbers,
animated,
scaleFactor,
value,
defaultValue,
onValueChange,
hideUndock,
})
}, [
id,
columns,
interactive,
showNumbers,
animated,
scaleFactor,
value,
defaultValue,
onValueChange,
hideUndock,
updateDockConfig,
])
// Track visibility with IntersectionObserver
useEffect(() => {
const element = containerRef.current
if (!element) return
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
// Consider visible if at least 20% is in view
updateDockVisibility(element, entry.isIntersecting && entry.intersectionRatio >= 0.2)
}
},
{
threshold: [0, 0.2, 0.5, 1.0],
}
)
observer.observe(element)
return () => observer.disconnect()
}, [updateDockVisibility])
// Default styles for the dock container
const defaultStyle: CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
...style,
}
return (
<div
ref={containerRef}
data-component="abacus-dock"
data-dock-id={id}
style={defaultStyle}
{...divProps}
/>
)
}
export default AbacusDock
|