Files
vue-bits/public/r/Dock.json
2026-01-21 16:08:55 +05:30

1 line
7.6 KiB
JSON

{"name":"Dock","title":"Dock","description":"macOS style magnifying dock with proximity scaling of icons.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onUnmounted, defineComponent, h } from 'vue';\nimport { useMotionValue, useSpring, useTransform } from 'motion-v';\n\nexport type SpringOptions = NonNullable<Parameters<typeof useSpring>[1]>;\n\nexport type DockItemData = {\n icon: unknown;\n label: unknown;\n onClick: () => void;\n className?: string;\n};\n\nexport type DockProps = {\n items: DockItemData[];\n className?: string;\n distance?: number;\n panelHeight?: number;\n baseItemSize?: number;\n dockHeight?: number;\n magnification?: number;\n spring?: SpringOptions;\n};\n\nconst props = withDefaults(defineProps<DockProps>(), {\n className: '',\n distance: 200,\n panelHeight: 64,\n baseItemSize: 50,\n dockHeight: 256,\n magnification: 70,\n spring: () => ({ mass: 0.1, stiffness: 150, damping: 12 })\n});\n\nconst mouseX = useMotionValue(Infinity);\nconst isHovered = useMotionValue(0);\nconst currentHeight = ref(props.panelHeight);\n\nconst maxHeight = computed(() => Math.max(props.dockHeight, props.magnification + props.magnification / 2 + 4));\n\nconst heightRow = useTransform(isHovered, [0, 1], [props.panelHeight, maxHeight.value]);\nconst height = useSpring(heightRow, props.spring);\n\nlet unsubscribeHeight: (() => void) | null = null;\n\nonMounted(() => {\n unsubscribeHeight = height.on('change', (latest: number) => {\n currentHeight.value = latest;\n });\n});\n\nonUnmounted(() => {\n if (unsubscribeHeight) {\n unsubscribeHeight();\n }\n});\n\nconst handleMouseMove = (event: MouseEvent) => {\n isHovered.set(1);\n mouseX.set(event.pageX);\n};\n\nconst handleMouseLeave = () => {\n isHovered.set(0);\n mouseX.set(Infinity);\n};\n</script>\n\n<template>\n <div :style=\"{ height: currentHeight + 'px', scrollbarWidth: 'none' }\" class=\"flex items-center mx-2 max-w-full\">\n <div\n @mousemove=\"handleMouseMove\"\n @mouseleave=\"handleMouseLeave\"\n :class=\"`${props.className} absolute bottom-2 left-1/2 transform -translate-x-1/2 flex items-end w-fit gap-4 rounded-2xl border-neutral-700 border-2 pb-2 px-4`\"\n :style=\"{ height: props.panelHeight + 'px' }\"\n role=\"toolbar\"\n aria-=\"Application dock\"\n >\n <DockItem\n v-for=\"(item, index) in props.items\"\n :key=\"index\"\n :onClick=\"item.onClick\"\n :className=\"item.className\"\n :mouseX=\"mouseX\"\n :spring=\"props.spring\"\n :distance=\"props.distance\"\n :magnification=\"props.magnification\"\n :baseItemSize=\"props.baseItemSize\"\n :item=\"item\"\n />\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nconst DockItem = defineComponent({\n name: 'DockItem',\n props: {\n className: {\n type: String,\n default: ''\n },\n onClick: {\n type: Function,\n default: () => {}\n },\n mouseX: {\n type: Object as () => ReturnType<typeof useMotionValue<number>>,\n required: true\n },\n spring: {\n type: Object as () => SpringOptions,\n required: true\n },\n distance: {\n type: Number,\n required: true\n },\n baseItemSize: {\n type: Number,\n required: true\n },\n magnification: {\n type: Number,\n required: true\n },\n item: {\n type: Object as () => DockItemData,\n required: true\n }\n },\n setup(props) {\n const itemRef = ref<HTMLDivElement>();\n const isHovered = useMotionValue(0);\n const currentSize = ref(props.baseItemSize);\n\n const mouseDistance = useTransform(props.mouseX, (val: number) => {\n const rect = itemRef.value?.getBoundingClientRect() ?? {\n x: 0,\n width: props.baseItemSize\n };\n return val - rect.x - props.baseItemSize / 2;\n });\n\n const targetSize = useTransform(\n mouseDistance,\n [-props.distance, 0, props.distance],\n [props.baseItemSize, props.magnification, props.baseItemSize]\n );\n const size = useSpring(targetSize, props.spring);\n\n let unsubscribeSize: (() => void) | null = null;\n\n onMounted(() => {\n unsubscribeSize = size.on('change', (latest: number) => {\n currentSize.value = latest;\n });\n });\n\n onUnmounted(() => {\n if (unsubscribeSize) {\n unsubscribeSize();\n }\n });\n\n const handleHoverStart = () => isHovered.set(1);\n const handleHoverEnd = () => isHovered.set(0);\n const handleFocus = () => isHovered.set(1);\n const handleBlur = () => isHovered.set(0);\n\n return {\n itemRef,\n size,\n currentSize,\n isHovered,\n handleHoverStart,\n handleHoverEnd,\n handleFocus,\n handleBlur\n };\n },\n render() {\n const icon = typeof this.item.icon === 'function' ? this.item.icon() : this.item.icon;\n const label = typeof this.item.label === 'function' ? this.item.label() : this.item.label;\n\n return h(\n 'div',\n {\n ref: 'itemRef',\n style: {\n width: this.currentSize + 'px',\n height: this.currentSize + 'px'\n },\n onMouseenter: this.handleHoverStart,\n onMouseleave: this.handleHoverEnd,\n onFocus: this.handleFocus,\n onBlur: this.handleBlur,\n onClick: this.onClick,\n class: `relative cursor-pointer inline-flex items-center justify-center rounded-full bg-[#111] border-neutral-700 border-2 shadow-md ${this.className}`,\n tabindex: 0,\n role: 'button',\n 'aria-haspopup': 'true'\n },\n [\n h(DockIcon, {}, () => [icon]),\n h(DockLabel, { isHovered: this.isHovered }, () => [typeof label === 'string' ? label : label])\n ]\n );\n }\n});\n\nconst DockLabel = defineComponent({\n name: 'DockLabel',\n props: {\n className: {\n type: String,\n default: ''\n },\n isHovered: {\n type: Object as () => ReturnType<typeof useMotionValue<number>>,\n required: true\n }\n },\n setup(props) {\n const isVisible = ref(false);\n\n let unsubscribe: (() => void) | null = null;\n\n onMounted(() => {\n unsubscribe = props.isHovered.on('change', (latest: number) => {\n isVisible.value = latest === 1;\n });\n });\n\n onUnmounted(() => {\n if (unsubscribe) {\n unsubscribe();\n }\n });\n\n return {\n isVisible\n };\n },\n render() {\n return h(\n 'div',\n {\n class: `${this.className} absolute -top-8 left-1/2 w-fit whitespace-pre rounded-md border border-neutral-700 bg-[#111] px-2 py-0.5 text-xs text-white transition-all duration-200`,\n role: 'tooltip',\n style: {\n transform: 'translateX(-50%)',\n opacity: this.isVisible ? 1 : 0,\n visibility: this.isVisible ? 'visible' : 'hidden'\n }\n },\n this.$slots.default?.()\n );\n }\n});\n\nconst DockIcon = defineComponent({\n name: 'DockIcon',\n props: {\n className: {\n type: String,\n default: ''\n }\n },\n render() {\n return h(\n 'div',\n {\n class: `flex items-center justify-center ${this.className}`\n },\n this.$slots.default?.()\n );\n }\n});\n\nexport default defineComponent({\n name: 'Dock',\n components: {\n DockItem\n }\n});\n</script>\n","path":"Dock/Dock.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.5.0"}],"devDependencies":[],"categories":["Components"]}